diff --git a/internal/app.go b/internal/app.go index b10c3e9..278792e 100644 --- a/internal/app.go +++ b/internal/app.go @@ -288,25 +288,8 @@ func autoMigrate(database *gorm.DB) error { // initDefaultProperties 初始化默认属性配置 func initDefaultProperties(ctx context.Context, components *AppComponents, logger *zap.Logger) error { - const propertyIDNotificationChannels = "notification_channels" - - // 检查是否已存在通知渠道配置 - _, err := components.PropertyService.Get(ctx, propertyIDNotificationChannels) - if err == nil { - // 配置已存在,跳过初始化 - logger.Info("通知渠道配置已存在,跳过初始化") - return nil - } - - // 创建默认空配置 - emptyChannels := make([]interface{}, 0) - err = components.PropertyService.Set(ctx, propertyIDNotificationChannels, "通知渠道配置", emptyChannels) - if err != nil { - return err - } - - logger.Info("默认通知渠道配置初始化成功") - return nil + // 使用 PropertyService 的初始化方法 + return components.PropertyService.InitializeDefaultConfigs(ctx) } func ErrorHandler(logger *zap.Logger) func(next echo.HandlerFunc) echo.HandlerFunc { diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index d0f6bd6..a4c8635 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -620,7 +620,7 @@ func (h *AgentHandler) SendCommand(c echo.Context) error { }) } -// GetAuditResult 获取审计结果 +// GetAuditResult 获取审计结果(原始数据) func (h *AgentHandler) GetAuditResult(c echo.Context) error { agentID := c.Param("id") ctx := c.Request().Context() diff --git a/internal/models/property.go b/internal/models/property.go index db9ac27..c1617a5 100644 --- a/internal/models/property.go +++ b/internal/models/property.go @@ -30,4 +30,5 @@ type SystemConfig struct { SystemNameZh string `json:"systemNameZh"` // 系统名称(中文) SystemNameEn string `json:"systemNameEn"` // 系统名称(英文) LogoBase64 string `json:"logoBase64"` // 系统logo(base64编码) + ICPCode string `json:"icpCode"` // ICP备案号 } diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 9c3d407..89c88cc 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -195,24 +195,36 @@ type CommandResponse struct { Result string `json:"result,omitempty"` // 结果数据(JSON字符串) } -// VPSAuditResult VPS安全审计结果 +// VPSAuditResult VPS资产采集结果(Agent端只负责采集,不做安全判断) type VPSAuditResult struct { // 系统信息 SystemInfo SystemInfo `json:"systemInfo"` + // 【核心】资产清单(Agent收集的原始数据) + AssetInventory AssetInventory `json:"assetInventory"` + // 统计摘要 + Statistics AuditStatistics `json:"statistics"` + // 采集开始时间 + StartTime int64 `json:"startTime"` + // 采集结束时间 + EndTime int64 `json:"endTime"` + // 采集警告(权限不足、命令失败等问题) + CollectWarnings []string `json:"collectWarnings,omitempty"` +} + +// VPSAuditAnalysis VPS安全分析结果(Server端分析后的结果) +type VPSAuditAnalysis struct { + // 关联的审计ID + AuditID string `json:"auditId"` // 安全检查结果 SecurityChecks []SecurityCheck `json:"securityChecks"` - // 审计开始时间 - StartTime int64 `json:"startTime"` - // 审计结束时间 - EndTime int64 `json:"endTime"` // 风险评分 (0-100) RiskScore int `json:"riskScore"` // 威胁等级: low/medium/high/critical ThreatLevel string `json:"threatLevel"` // 修复建议 Recommendations []string `json:"recommendations,omitempty"` - // 审计警告(权限不足、检查失败等非安全问题) - AuditWarnings []string `json:"auditWarnings,omitempty"` + // 分析时间 + AnalyzedAt int64 `json:"analyzedAt"` } // SystemInfo 系统信息 @@ -234,10 +246,11 @@ type SecurityCheck struct { // SecurityCheckSub 安全检查子项 type SecurityCheckSub struct { - Name string `json:"name"` // 子检查名称 - Status string `json:"status"` // pass/fail/warn/skip - Message string `json:"message"` // 检查消息 - Evidence *Evidence `json:"evidence,omitempty"` // 证据信息 + Name string `json:"name"` // 子检查名称 + Status string `json:"status"` // pass/fail/warn/skip + Severity string `json:"severity,omitempty"` // 严重程度: high/medium/low + Message string `json:"message"` // 检查消息 + Evidence string `json:"evidence,omitempty"` // 证据信息(简化为字符串) } // Evidence 安全事件证据 @@ -298,3 +311,294 @@ type TamperAlertData struct { Details string `json:"details"` // 详细信息(如: "不可变属性被移除") Restored bool `json:"restored"` // 是否已自动恢复 } + +// ==================== 资产清单相关数据结构 ==================== + +// AssetInventory 资产清单 +type AssetInventory struct { + NetworkAssets *NetworkAssets `json:"networkAssets,omitempty"` // 网络资产 + ProcessAssets *ProcessAssets `json:"processAssets,omitempty"` // 进程资产 + UserAssets *UserAssets `json:"userAssets,omitempty"` // 用户资产 + FileAssets *FileAssets `json:"fileAssets,omitempty"` // 文件资产 + KernelAssets *KernelAssets `json:"kernelAssets,omitempty"` // 内核资产 +} + +// AuditStatistics 审计统计摘要 +type AuditStatistics struct { + NetworkStats *NetworkStatistics `json:"networkStats,omitempty"` // 网络统计 + ProcessStats *ProcessStatistics `json:"processStats,omitempty"` // 进程统计 + UserStats *UserStatistics `json:"userStats,omitempty"` // 用户统计 + FileStats *FileStatistics `json:"fileStats,omitempty"` // 文件统计 +} + +// ==================== 网络资产 ==================== + +// NetworkAssets 网络资产 +type NetworkAssets struct { + ListeningPorts []ListeningPort `json:"listeningPorts,omitempty"` // 监听端口 + Connections []NetworkConnection `json:"connections,omitempty"` // 网络连接 + Interfaces []NetworkInterface `json:"interfaces,omitempty"` // 网卡接口 + RoutingTable []RouteEntry `json:"routingTable,omitempty"` // 路由表 + FirewallRules *FirewallInfo `json:"firewallRules,omitempty"` // 防火墙规则 + DNSServers []string `json:"dnsServers,omitempty"` // DNS服务器 + ARPTable []ARPEntry `json:"arpTable,omitempty"` // ARP表 + Statistics *NetworkStatistics `json:"statistics,omitempty"` // 统计信息 +} + +// ListeningPort 监听端口 +type ListeningPort struct { + Protocol string `json:"protocol"` // tcp/udp + Address string `json:"address"` // 0.0.0.0/127.0.0.1/:: + Port uint32 `json:"port"` // 端口号 + ProcessPID int32 `json:"processPid"` // 进程PID + ProcessName string `json:"processName,omitempty"` // 进程名 + ProcessPath string `json:"processPath,omitempty"` // 进程路径 + IsPublic bool `json:"isPublic"` // 是否公网监听 +} + +// NetworkConnection 网络连接 +type NetworkConnection struct { + Protocol string `json:"protocol"` // tcp/udp + LocalAddr string `json:"localAddr"` // 本地地址 + LocalPort uint32 `json:"localPort"` // 本地端口 + RemoteAddr string `json:"remoteAddr"` // 远程地址 + RemotePort uint32 `json:"remotePort"` // 远程端口 + State string `json:"state"` // ESTABLISHED/LISTEN/... + ProcessPID int32 `json:"processPid"` // 进程PID + ProcessName string `json:"processName,omitempty"` // 进程名 +} + +// NetworkInterface 网卡接口 +type NetworkInterface struct { + Name string `json:"name"` // 接口名 + MacAddress string `json:"macAddress"` // MAC地址 + Addresses []string `json:"addresses,omitempty"` // IP地址列表 + MTU int `json:"mtu"` // MTU + IsUp bool `json:"isUp"` // 是否启用 + Flags []string `json:"flags,omitempty"` // 标志 +} + +// RouteEntry 路由表条目 +type RouteEntry struct { + Destination string `json:"destination"` // 目标网络 + Gateway string `json:"gateway"` // 网关 + Genmask string `json:"genmask"` // 子网掩码 + Interface string `json:"interface"` // 接口 + Metric int `json:"metric"` // 优先级 +} + +// FirewallInfo 防火墙信息 +type FirewallInfo struct { + Type string `json:"type"` // iptables/ufw/firewalld + Status string `json:"status"` // active/inactive + Rules []FirewallRule `json:"rules,omitempty"` // 规则列表 +} + +// FirewallRule 防火墙规则 +type FirewallRule struct { + Chain string `json:"chain"` // 链名 + Target string `json:"target"` // 目标动作 + Protocol string `json:"protocol,omitempty"` // 协议 + Source string `json:"source,omitempty"` // 源地址 + Dest string `json:"dest,omitempty"` // 目标地址 + Port string `json:"port,omitempty"` // 端口 +} + +// ARPEntry ARP表条目 +type ARPEntry struct { + IPAddress string `json:"ipAddress"` // IP地址 + MacAddress string `json:"macAddress"` // MAC地址 + Interface string `json:"interface"` // 接口 +} + +// NetworkStatistics 网络统计 +type NetworkStatistics struct { + TotalListeningPorts int `json:"totalListeningPorts"` // 总监听端口数 + PublicListeningPorts int `json:"publicListeningPorts"` // 公网监听端口数 + ActiveConnections int `json:"activeConnections"` // 活跃连接数 + ConnectionsByState map[string]int `json:"connectionsByState,omitempty"` // 连接状态分布 + InterfaceCount int `json:"interfaceCount"` // 网卡数量 +} + +// ==================== 进程资产 ==================== + +// ProcessAssets 进程资产 +type ProcessAssets struct { + RunningProcesses []ProcessInfo `json:"runningProcesses,omitempty"` // 所有运行进程(可选) + TopCPUProcesses []ProcessInfo `json:"topCpuProcesses,omitempty"` // CPU占用TOP进程 + TopMemoryProcesses []ProcessInfo `json:"topMemoryProcesses,omitempty"` // 内存占用TOP进程 + Statistics *ProcessStatistics `json:"statistics,omitempty"` // 统计信息 +} + +// ProcessInfo 进程信息 +type ProcessInfo struct { + PID int32 `json:"pid"` // 进程ID + Name string `json:"name"` // 进程名 + Cmdline string `json:"cmdline,omitempty"` // 命令行 + Exe string `json:"exe,omitempty"` // 可执行文件路径 + PPID int32 `json:"ppid"` // 父进程ID + Username string `json:"username,omitempty"` // 用户名 + CPUPercent float64 `json:"cpuPercent"` // CPU使用率 + MemPercent float32 `json:"memPercent"` // 内存使用率 + MemoryMB uint64 `json:"memoryMb"` // 内存占用(MB) + Status string `json:"status,omitempty"` // 状态 + CreateTime int64 `json:"createTime"` // 创建时间(毫秒) +} + +// ProcessStatistics 进程统计 +type ProcessStatistics struct { + TotalProcesses int `json:"totalProcesses"` // 进程总数 + RunningProcesses int `json:"runningProcesses"` // 运行中进程 + SleepingProcesses int `json:"sleepingProcesses"` // 睡眠进程 + ZombieProcesses int `json:"zombieProcesses"` // 僵尸进程 + ThreadCount int `json:"threadCount"` // 线程总数 +} + +// ==================== 用户资产 ==================== + +// UserAssets 用户资产 +type UserAssets struct { + SystemUsers []UserInfo `json:"systemUsers,omitempty"` // 系统用户 + LoginHistory []LoginRecord `json:"loginHistory,omitempty"` // 登录历史 + CurrentLogins []LoginSession `json:"currentLogins,omitempty"` // 当前登录 + SSHKeys []SSHKeyInfo `json:"sshKeys,omitempty"` // SSH密钥 + SudoUsers []SudoUserInfo `json:"sudoUsers,omitempty"` // Sudo用户 + Statistics *UserStatistics `json:"statistics,omitempty"` // 统计信息 +} + +// UserInfo 用户信息 +type UserInfo struct { + Username string `json:"username"` // 用户名 + UID string `json:"uid"` // UID + GID string `json:"gid"` // GID + HomeDir string `json:"homeDir,omitempty"` // 家目录 + Shell string `json:"shell,omitempty"` // Shell + IsLoginable bool `json:"isLoginable"` // 是否可登录 + IsRootEquiv bool `json:"isRootEquiv,omitempty"` // 是否UID=0 + HasPassword bool `json:"hasPassword"` // 是否有密码 +} + +// LoginRecord 登录记录 +type LoginRecord struct { + Username string `json:"username"` // 用户名 + IP string `json:"ip,omitempty"` // IP地址 + Terminal string `json:"terminal"` // 终端 + Timestamp int64 `json:"timestamp"` // 时间戳(毫秒) + Status string `json:"status,omitempty"` // success/failed +} + +// LoginSession 登录会话 +type LoginSession struct { + Username string `json:"username"` // 用户名 + Terminal string `json:"terminal"` // 终端 + IP string `json:"ip"` // IP地址 + LoginTime int64 `json:"loginTime"` // 登录时间(毫秒) + IdleTime int `json:"idleTime"` // 空闲时间(秒) +} + +// SSHKeyInfo SSH密钥信息 +type SSHKeyInfo struct { + Username string `json:"username"` // 用户名 + KeyType string `json:"keyType"` // 密钥类型 + Fingerprint string `json:"fingerprint"` // 指纹 + Comment string `json:"comment,omitempty"` // 注释 + FilePath string `json:"filePath"` // 文件路径 + AddedTime int64 `json:"addedTime,omitempty"` // 添加时间(毫秒) +} + +// SudoUserInfo Sudo用户信息 +type SudoUserInfo struct { + Username string `json:"username"` // 用户名 + Rules string `json:"rules,omitempty"` // 规则 + NoPasswd bool `json:"noPasswd"` // 是否免密 +} + +// UserStatistics 用户统计 +type UserStatistics struct { + TotalUsers int `json:"totalUsers"` // 用户总数 + LoginableUsers int `json:"loginableUsers"` // 可登录用户数 + RootEquivalentUsers int `json:"rootEquivalentUsers"` // Root权限用户数 + RecentLoginCount int `json:"recentLoginCount"` // 近期登录次数 + FailedLoginCount int `json:"failedLoginCount"` // 失败登录次数 +} + +// ==================== 文件资产 ==================== + +// FileAssets 文件资产 +type FileAssets struct { + CronJobs []CronJob `json:"cronJobs,omitempty"` // 定时任务 + SystemdServices []SystemdService `json:"systemdServices,omitempty"` // Systemd服务 + StartupScripts []StartupScript `json:"startupScripts,omitempty"` // 启动脚本 + RecentModified []FileInfo `json:"recentModified,omitempty"` // 最近修改文件 + LargeFiles []FileInfo `json:"largeFiles,omitempty"` // 大文件 + Statistics *FileStatistics `json:"statistics,omitempty"` // 统计信息 +} + +// CronJob 定时任务 +type CronJob struct { + User string `json:"user"` // 用户 + Schedule string `json:"schedule"` // 计划 + Command string `json:"command"` // 命令 + FilePath string `json:"filePath,omitempty"` // 文件路径 +} + +// SystemdService Systemd服务 +type SystemdService struct { + Name string `json:"name"` // 服务名 + State string `json:"state,omitempty"` // 状态 + Enabled bool `json:"enabled"` // 是否开机启动 + ExecStart string `json:"execStart,omitempty"` // 启动命令 + Description string `json:"description,omitempty"` // 描述 + UnitFile string `json:"unitFile,omitempty"` // Unit文件路径 +} + +// StartupScript 启动脚本 +type StartupScript struct { + Type string `json:"type"` // init.d/rc.local/systemd + Path string `json:"path"` // 路径 + Name string `json:"name"` // 名称 + Enabled bool `json:"enabled"` // 是否启用 +} + +// FileInfo 文件信息 +type FileInfo struct { + Path string `json:"path"` // 路径 + Size int64 `json:"size"` // 大小(字节) + ModTime int64 `json:"modTime"` // 修改时间(毫秒) + Permissions string `json:"permissions,omitempty"` // 权限 + Owner string `json:"owner,omitempty"` // 所有者 + Group string `json:"group,omitempty"` // 组 + IsExecutable bool `json:"isExecutable"` // 是否可执行 +} + +// FileStatistics 文件统计 +type FileStatistics struct { + CronJobsCount int `json:"cronJobsCount"` // 定时任务数量 + SystemdServicesCount int `json:"systemdServicesCount"` // Systemd服务数量 + ActiveServicesCount int `json:"activeServicesCount"` // 活跃服务数量 + RecentFilesCount int `json:"recentFilesCount"` // 最近修改文件数量 + LargeFilesCount int `json:"largeFilesCount"` // 大文件数量 +} + +// ==================== 内核资产 ==================== + +// KernelAssets 内核资产 +type KernelAssets struct { + LoadedModules []KernelModule `json:"loadedModules,omitempty"` // 已加载内核模块 + KernelParameters map[string]string `json:"kernelParameters,omitempty"` // 内核参数 + SecurityModules *SecurityModuleInfo `json:"securityModules,omitempty"` // 安全模块 +} + +// KernelModule 内核模块 +type KernelModule struct { + Name string `json:"name"` // 模块名 + Size int `json:"size"` // 大小 + UsedBy int `json:"usedBy"` // 被引用次数 +} + +// SecurityModuleInfo 安全模块信息 +type SecurityModuleInfo struct { + SELinuxStatus string `json:"selinuxStatus,omitempty"` // SELinux状态 + AppArmorStatus string `json:"apparmorStatus,omitempty"` // AppArmor状态 + SecureBootState string `json:"secureBootState,omitempty"` // 安全启动状态 +} diff --git a/internal/repo/agent_repo.go b/internal/repo/agent_repo.go index 4fe9c58..29028e7 100644 --- a/internal/repo/agent_repo.go +++ b/internal/repo/agent_repo.go @@ -95,6 +95,19 @@ func (r *AgentRepo) GetLatestAuditResult(ctx context.Context, agentID string) (* return &audit, nil } +// GetLatestAuditResultByType 根据类型获取最新的审计结果 +func (r *AgentRepo) GetLatestAuditResultByType(ctx context.Context, agentID string, resultType string) (*models.AuditResult, error) { + var audit models.AuditResult + err := r.db.WithContext(ctx). + Where("agent_id = ? AND type = ?", agentID, resultType). + Order("created_at DESC"). + First(&audit).Error + if err != nil { + return nil, err + } + return &audit, nil +} + // ListAuditResults 获取审计结果列表 func (r *AgentRepo) ListAuditResults(ctx context.Context, agentID string) ([]models.AuditResult, error) { var audits []models.AuditResult diff --git a/internal/repo/property_repo.go b/internal/repo/property_repo.go index b96e80f..851a6c2 100644 --- a/internal/repo/property_repo.go +++ b/internal/repo/property_repo.go @@ -1,8 +1,6 @@ package repo import ( - "context" - "github.com/dushixiang/pika/internal/models" "github.com/go-orz/orz" "gorm.io/gorm" @@ -19,42 +17,3 @@ func NewPropertyRepo(db *gorm.DB) *PropertyRepo { db: db, } } - -// Get 获取属性 -func (r *PropertyRepo) Get(ctx context.Context, id string) (*models.Property, error) { - var property models.Property - err := r.db.WithContext(ctx).Where("id = ?", id).First(&property).Error - if err != nil { - return nil, err - } - return &property, nil -} - -// Set 设置属性 -func (r *PropertyRepo) Set(ctx context.Context, property *models.Property) error { - // 先尝试查找 - var existing models.Property - err := r.db.WithContext(ctx).Where("id = ?", property.ID).First(&existing).Error - - if err == gorm.ErrRecordNotFound { - // 不存在,创建 - return r.db.WithContext(ctx).Create(property).Error - } else if err != nil { - return err - } - - // 存在,更新 - return r.db.WithContext(ctx).Model(&existing).Updates(property).Error -} - -// Delete 删除属性 -func (r *PropertyRepo) Delete(ctx context.Context, id string) error { - return r.db.WithContext(ctx).Delete(&models.Property{}, "id = ?", id).Error -} - -// List 列出所有属性 -func (r *PropertyRepo) List(ctx context.Context) ([]models.Property, error) { - var properties []models.Property - err := r.db.WithContext(ctx).Find(&properties).Error - return properties, err -} diff --git a/internal/service/agent_service.go b/internal/service/agent_service.go index 1af1f4b..14f0fed 100644 --- a/internal/service/agent_service.go +++ b/internal/service/agent_service.go @@ -633,12 +633,21 @@ func (s *AgentService) SaveAuditResult(ctx context.Context, agentID string, resu } // 保存到数据库 - return s.AgentRepo.SaveAuditResult(ctx, auditRecord) + if err := s.AgentRepo.SaveAuditResult(ctx, auditRecord); err != nil { + return err + } + + s.logger.Info("审计结果保存成功", + zap.String("agentId", agentID), + zap.Int64("auditId", auditRecord.ID), + ) + + return nil } -// GetAuditResult 获取最新的审计结果 +// GetAuditResult 获取最新的审计结果(原始数据) func (s *AgentService) GetAuditResult(ctx context.Context, agentID string) (*protocol.VPSAuditResult, error) { - record, err := s.AgentRepo.GetLatestAuditResult(ctx, agentID) + record, err := s.AgentRepo.GetLatestAuditResultByType(ctx, agentID, "vps_audit") if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil @@ -669,33 +678,19 @@ func (s *AgentService) ListAuditResults(ctx context.Context, agentID string) ([] continue } - // 统计检查结果 - passCount := 0 - failCount := 0 - warnCount := 0 - for _, check := range auditResult.SecurityChecks { - switch check.Status { - case "pass": - passCount++ - case "fail": - failCount++ - case "warn": - warnCount++ - } - } + // TODO: 统计安全检查结果应该来自 Server 端分析后的 VPSAuditAnalysis + // Agent 端已经不再产生 SecurityChecks,需要实现 Server 端分析逻辑 results = append(results, map[string]interface{}{ - "id": record.ID, - "agentId": record.AgentID, - "type": record.Type, - "startTime": record.StartTime, - "endTime": record.EndTime, - "createdAt": record.CreatedAt, - "passCount": passCount, - "failCount": failCount, - "warnCount": warnCount, - "totalCount": len(auditResult.SecurityChecks), - "systemInfo": auditResult.SystemInfo, + "id": record.ID, + "agentId": record.AgentID, + "type": record.Type, + "startTime": record.StartTime, + "endTime": record.EndTime, + "createdAt": record.CreatedAt, + "systemInfo": auditResult.SystemInfo, + "statistics": auditResult.Statistics, + "collectTime": auditResult.EndTime - auditResult.StartTime, }) } diff --git a/internal/service/property_service.go b/internal/service/property_service.go index de0c4d8..2ad2eb8 100644 --- a/internal/service/property_service.go +++ b/internal/service/property_service.go @@ -8,6 +8,7 @@ import ( "github.com/dushixiang/pika/internal/models" "github.com/dushixiang/pika/internal/repo" + "github.com/dushixiang/pika/web" "go.uber.org/zap" "gorm.io/gorm" ) @@ -32,13 +33,13 @@ func NewPropertyService(logger *zap.Logger, db *gorm.DB) *PropertyService { } // Get 获取属性(返回原始 JSON 字符串) -func (s *PropertyService) Get(ctx context.Context, id string) (*models.Property, error) { - return s.repo.Get(ctx, id) +func (s *PropertyService) Get(ctx context.Context, id string) (models.Property, error) { + return s.repo.FindById(ctx, id) } // GetValue 获取属性值并反序列化 func (s *PropertyService) GetValue(ctx context.Context, id string, target interface{}) error { - property, err := s.repo.Get(ctx, id) + property, err := s.repo.FindById(ctx, id) if err != nil { return err } @@ -65,12 +66,12 @@ func (s *PropertyService) Set(ctx context.Context, id string, name string, value UpdatedAt: time.Now().UnixMilli(), } - return s.repo.Set(ctx, property) + return s.repo.Save(ctx, property) } // Delete 删除属性 func (s *PropertyService) Delete(ctx context.Context, id string) error { - return s.repo.Delete(ctx, id) + return s.repo.DeleteById(ctx, id) } func (s *PropertyService) GetNotificationChannelConfigs(ctx context.Context) ([]models.NotificationChannelConfig, error) { @@ -90,3 +91,64 @@ func (s *PropertyService) GetSystemConfig(ctx context.Context) (*models.SystemCo } return &systemConfig, nil } + +// defaultPropertyConfig 默认配置项定义 +type defaultPropertyConfig struct { + ID string + Name string + Value interface{} +} + +// InitializeDefaultConfigs 初始化默认配置(如果数据库中不存在) +func (s *PropertyService) InitializeDefaultConfigs(ctx context.Context) error { + // 定义所有需要初始化的默认配置 + defaultConfigs := []defaultPropertyConfig{ + { + ID: PropertyIDSystemConfig, + Name: "系统配置", + Value: models.SystemConfig{ + SystemNameEn: "Pika Monitor", + SystemNameZh: "皮卡监控", + LogoBase64: web.DefaultLogoBase64(), + ICPCode: "", + }, + }, + { + ID: PropertyIDNotificationChannels, + Name: "通知渠道配置", + Value: []models.NotificationChannelConfig{}, + }, + } + + // 遍历并初始化每个配置 + for _, config := range defaultConfigs { + if err := s.initializeProperty(ctx, config); err != nil { + return fmt.Errorf("初始化 %s 失败: %w", config.Name, err) + } + } + + s.logger.Info("默认配置初始化完成") + return nil +} + +// initializeProperty 初始化单个配置项 +func (s *PropertyService) initializeProperty(ctx context.Context, config defaultPropertyConfig) error { + // 检查配置是否已存在 + exists, err := s.repo.ExistsById(ctx, config.ID) + if err != nil { + return err + } + + if exists { + // 配置已存在,无需初始化 + s.logger.Info("配置已存在,跳过初始化", zap.String("name", config.Name)) + return nil + } + + // 配置不存在,创建默认配置 + if err := s.Set(ctx, config.ID, config.Name, config.Value); err != nil { + return err + } + s.logger.Info("配置默认值已初始化", zap.String("name", config.Name)) + return nil +} diff --git a/pkg/agent/audit/asset_file.go b/pkg/agent/audit/asset_file.go new file mode 100644 index 0000000..f2d5999 --- /dev/null +++ b/pkg/agent/audit/asset_file.go @@ -0,0 +1,340 @@ +package audit + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" +) + +// FileAssetsCollector 文件资产收集器 +type FileAssetsCollector struct { + config *Config + executor *CommandExecutor +} + +// NewFileAssetsCollector 创建文件资产收集器 +func NewFileAssetsCollector(config *Config, executor *CommandExecutor) *FileAssetsCollector { + return &FileAssetsCollector{ + config: config, + executor: executor, + } +} + +// Collect 收集文件资产 +func (fac *FileAssetsCollector) Collect() *protocol.FileAssets { + assets := &protocol.FileAssets{} + + // 收集Cron任务 + assets.CronJobs = fac.collectCronJobs() + + // 收集Systemd服务 + assets.SystemdServices = fac.collectSystemdServices() + + // 收集启动脚本 + assets.StartupScripts = fac.collectStartupScripts() + + // 收集最近修改文件 + assets.RecentModified = fac.collectRecentModified() + + // 收集大文件 + assets.LargeFiles = fac.collectLargeFiles() + + // 统计信息 + assets.Statistics = fac.calculateStatistics(assets) + + return assets +} + +// collectCronJobs 收集Cron任务 +func (fac *FileAssetsCollector) collectCronJobs() []protocol.CronJob { + var jobs []protocol.CronJob + + // 收集 /etc/crontab + jobs = append(jobs, fac.parseCronFile("/etc/crontab", "root")...) + + // 收集 /etc/cron.d/* + cronDFiles, _ := filepath.Glob("/etc/cron.d/*") + for _, file := range cronDFiles { + jobs = append(jobs, fac.parseCronFile(file, "root")...) + } + + // 收集用户crontab + userCronFiles, _ := filepath.Glob("/var/spool/cron/*") + for _, file := range userCronFiles { + username := filepath.Base(file) + jobs = append(jobs, fac.parseCronFile(file, username)...) + } + + // 限制数量 + if len(jobs) > 100 { + jobs = jobs[:100] + } + + return jobs +} + +// parseCronFile 解析Cron文件 +func (fac *FileAssetsCollector) parseCronFile(filePath string, defaultUser string) []protocol.CronJob { + var jobs []protocol.CronJob + + file, err := os.Open(filePath) + if err != nil { + return jobs + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 跳过环境变量定义 + if strings.Contains(line, "=") && !strings.Contains(line, " ") { + continue + } + + fields := strings.Fields(line) + if len(fields) < 6 { + continue + } + + // 判断是否包含用户字段 + user := defaultUser + schedule := "" + command := "" + + // /etc/crontab 格式: min hour day month dow user command + if filePath == "/etc/crontab" && len(fields) >= 7 { + schedule = strings.Join(fields[0:5], " ") + user = fields[5] + command = strings.Join(fields[6:], " ") + } else { + // 用户crontab格式: min hour day month dow command + schedule = strings.Join(fields[0:5], " ") + command = strings.Join(fields[5:], " ") + } + + job := protocol.CronJob{ + User: user, + Schedule: schedule, + Command: command, + FilePath: filePath, + } + + jobs = append(jobs, job) + } + + return jobs +} + +// collectSystemdServices 收集Systemd服务 +func (fac *FileAssetsCollector) collectSystemdServices() []protocol.SystemdService { + var services []protocol.SystemdService + + // 使用 systemctl list-units + output, err := fac.executor.Execute("systemctl", "list-units", "--type=service", "--all", "--no-pager") + if err != nil { + globalLogger.Debug("获取systemd服务失败: %v", err) + return services + } + + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "UNIT") || strings.HasPrefix(line, "●") { + continue + } + + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + + serviceName := fields[0] + if !strings.HasSuffix(serviceName, ".service") { + continue + } + + state := "" + if len(fields) > 2 { + state = fields[2] + } + + service := protocol.SystemdService{ + Name: serviceName, + State: state, + Enabled: strings.Contains(line, "enabled"), + } + + services = append(services, service) + + // 限制数量 + if len(services) >= 100 { + break + } + } + + return services +} + +// collectStartupScripts 收集启动脚本 +func (fac *FileAssetsCollector) collectStartupScripts() []protocol.StartupScript { + var scripts []protocol.StartupScript + + // 收集 /etc/init.d/* + initDFiles, _ := filepath.Glob("/etc/init.d/*") + for _, file := range initDFiles { + info, err := os.Stat(file) + if err != nil || info.IsDir() { + continue + } + + // 检查是否可执行 + if info.Mode()&0111 != 0 { + script := protocol.StartupScript{ + Type: "init.d", + Path: file, + Name: filepath.Base(file), + Enabled: true, // 简化,默认认为启用 + } + scripts = append(scripts, script) + } + } + + // 收集 /etc/rc.local + if info, err := os.Stat("/etc/rc.local"); err == nil && !info.IsDir() { + script := protocol.StartupScript{ + Type: "rc.local", + Path: "/etc/rc.local", + Name: "rc.local", + Enabled: info.Mode()&0111 != 0, + } + scripts = append(scripts, script) + } + + return scripts +} + +// collectRecentModified 收集最近修改文件 +func (fac *FileAssetsCollector) collectRecentModified() []protocol.FileInfo { + var files []protocol.FileInfo + + // 搜索最近7天修改的文件 + searchDirs := []string{"/etc", "/bin", "/sbin", "/usr/bin", "/usr/sbin"} + cutoffTime := time.Now().AddDate(0, 0, -7) + + for _, dir := range searchDirs { + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + // 跳过符号链接 + if info.Mode()&os.ModeSymlink != 0 { + return nil + } + + // 检查修改时间 + if info.ModTime().After(cutoffTime) { + fileInfo := fac.convertToFileInfo(path, info) + files = append(files, fileInfo) + } + + // 限制数量 + if len(files) >= 50 { + return filepath.SkipDir + } + + return nil + }) + + if len(files) >= 50 { + break + } + } + + return files +} + +// collectLargeFiles 收集大文件 +func (fac *FileAssetsCollector) collectLargeFiles() []protocol.FileInfo { + var files []protocol.FileInfo + + // 搜索大于200MB的文件 + searchDirs := []string{"/tmp", "/var/tmp", "/home", "/root"} + sizeThrreshold := int64(200 * 1024 * 1024) // 200MB + + for _, dir := range searchDirs { + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + // 跳过符号链接 + if info.Mode()&os.ModeSymlink != 0 { + return nil + } + + // 检查文件大小 + if info.Size() > sizeThrreshold { + fileInfo := fac.convertToFileInfo(path, info) + files = append(files, fileInfo) + } + + // 限制数量 + if len(files) >= 20 { + return filepath.SkipDir + } + + return nil + }) + + if len(files) >= 20 { + break + } + } + + return files +} + +// convertToFileInfo 转换为文件信息 +func (fac *FileAssetsCollector) convertToFileInfo(path string, info os.FileInfo) protocol.FileInfo { + fileInfo := protocol.FileInfo{ + Path: path, + Size: info.Size(), + ModTime: info.ModTime().UnixMilli(), + Permissions: fmt.Sprintf("%o", info.Mode().Perm()), + IsExecutable: info.Mode()&0111 != 0, + } + + // 获取所有者和组 (平台特定实现) + fac.fillFileOwnership(&fileInfo, info) + + return fileInfo +} + +// calculateStatistics 计算统计信息 +func (fac *FileAssetsCollector) calculateStatistics(assets *protocol.FileAssets) *protocol.FileStatistics { + stats := &protocol.FileStatistics{ + CronJobsCount: len(assets.CronJobs), + SystemdServicesCount: len(assets.SystemdServices), + RecentFilesCount: len(assets.RecentModified), + LargeFilesCount: len(assets.LargeFiles), + } + + // 统计活跃服务 + for _, service := range assets.SystemdServices { + if service.State == "running" || service.State == "active" { + stats.ActiveServicesCount++ + } + } + + return stats +} diff --git a/pkg/agent/audit/asset_file_unix.go b/pkg/agent/audit/asset_file_unix.go new file mode 100644 index 0000000..dbf1411 --- /dev/null +++ b/pkg/agent/audit/asset_file_unix.go @@ -0,0 +1,19 @@ +//go:build linux || darwin + +package audit + +import ( + "fmt" + "os" + "syscall" + + "github.com/dushixiang/pika/internal/protocol" +) + +// fillFileOwnership 填充文件所有者和组信息 (Unix系统) +func (fac *FileAssetsCollector) fillFileOwnership(fileInfo *protocol.FileInfo, info os.FileInfo) { + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + fileInfo.Owner = fmt.Sprintf("%d", stat.Uid) + fileInfo.Group = fmt.Sprintf("%d", stat.Gid) + } +} diff --git a/pkg/agent/audit/asset_file_windows.go b/pkg/agent/audit/asset_file_windows.go new file mode 100644 index 0000000..4717fa8 --- /dev/null +++ b/pkg/agent/audit/asset_file_windows.go @@ -0,0 +1,16 @@ +//go:build windows + +package audit + +import ( + "os" + + "github.com/dushixiang/pika/internal/protocol" +) + +// fillFileOwnership 填充文件所有者和组信息 (Windows系统) +// Windows 系统不支持 Unix 风格的 UID/GID,这里留空 +func (fac *FileAssetsCollector) fillFileOwnership(fileInfo *protocol.FileInfo, info os.FileInfo) { + // Windows 系统不填充 Owner 和 Group 字段 + // 可以在未来扩展以支持 Windows SID +} diff --git a/pkg/agent/audit/asset_kernel.go b/pkg/agent/audit/asset_kernel.go new file mode 100644 index 0000000..2e3e855 --- /dev/null +++ b/pkg/agent/audit/asset_kernel.go @@ -0,0 +1,153 @@ +package audit + +import ( + "bufio" + "os" + "strconv" + "strings" + + "github.com/dushixiang/pika/internal/protocol" +) + +// KernelAssetsCollector 内核资产收集器 +type KernelAssetsCollector struct { + config *Config + executor *CommandExecutor +} + +// NewKernelAssetsCollector 创建内核资产收集器 +func NewKernelAssetsCollector(config *Config, executor *CommandExecutor) *KernelAssetsCollector { + return &KernelAssetsCollector{ + config: config, + executor: executor, + } +} + +// Collect 收集内核资产 +func (kac *KernelAssetsCollector) Collect() *protocol.KernelAssets { + assets := &protocol.KernelAssets{} + + // 收集已加载内核模块 + assets.LoadedModules = kac.collectLoadedModules() + + // 收集内核参数 (选择性收集,避免过多) + assets.KernelParameters = kac.collectKernelParameters() + + // 收集安全模块信息 + assets.SecurityModules = kac.collectSecurityModules() + + return assets +} + +// collectLoadedModules 收集已加载内核模块 +func (kac *KernelAssetsCollector) collectLoadedModules() []protocol.KernelModule { + var modules []protocol.KernelModule + + file, err := os.Open("/proc/modules") + if err != nil { + globalLogger.Warn("读取/proc/modules失败: %v", err) + return modules + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 3 { + continue + } + + name := fields[0] + size, _ := strconv.Atoi(fields[1]) + usedBy, _ := strconv.Atoi(fields[2]) + + module := protocol.KernelModule{ + Name: name, + Size: size, + UsedBy: usedBy, + } + + modules = append(modules, module) + + // 限制数量,避免过多 + if len(modules) >= 100 { + break + } + } + + return modules +} + +// collectKernelParameters 收集内核参数 +func (kac *KernelAssetsCollector) collectKernelParameters() map[string]string { + params := make(map[string]string) + + // 只收集关键的安全相关参数 + keyParams := []string{ + "/proc/sys/net/ipv4/ip_forward", + "/proc/sys/kernel/randomize_va_space", + "/proc/sys/kernel/dmesg_restrict", + "/proc/sys/kernel/kptr_restrict", + "/proc/sys/kernel/yama/ptrace_scope", + } + + for _, paramPath := range keyParams { + if content, err := os.ReadFile(paramPath); err == nil { + // 提取参数名 + paramName := strings.TrimPrefix(paramPath, "/proc/sys/") + paramName = strings.ReplaceAll(paramName, "/", ".") + params[paramName] = strings.TrimSpace(string(content)) + } + } + + return params +} + +// collectSecurityModules 收集安全模块信息 +func (kac *KernelAssetsCollector) collectSecurityModules() *protocol.SecurityModuleInfo { + info := &protocol.SecurityModuleInfo{} + + // 检查SELinux + if content, err := os.ReadFile("/sys/fs/selinux/enforce"); err == nil { + enforce := strings.TrimSpace(string(content)) + if enforce == "1" { + info.SELinuxStatus = "enforcing" + } else if enforce == "0" { + info.SELinuxStatus = "permissive" + } + } else { + // 检查是否禁用 + if _, err := os.Stat("/sys/fs/selinux"); os.IsNotExist(err) { + info.SELinuxStatus = "disabled" + } + } + + // 检查AppArmor + _, err := kac.executor.Execute("aa-status", "--enabled") + if err == nil { + info.AppArmorStatus = "enabled" + } else { + // 检查是否安装 + whichOutput, whichErr := kac.executor.Execute("which", "aa-status") + if whichErr != nil || whichOutput == "" { + info.AppArmorStatus = "not_installed" + } else { + info.AppArmorStatus = "disabled" + } + } + + // 检查Secure Boot (通过 mokutil 或 /sys/firmware/efi) + if content, err := os.ReadFile("/sys/firmware/efi/efivars/SecureBoot-*"); err == nil { + // 简化判断 + if len(content) > 0 { + info.SecureBootState = "enabled" + } else { + info.SecureBootState = "disabled" + } + } else { + info.SecureBootState = "unknown" + } + + return info +} diff --git a/pkg/agent/audit/asset_network.go b/pkg/agent/audit/asset_network.go new file mode 100644 index 0000000..90706dd --- /dev/null +++ b/pkg/agent/audit/asset_network.go @@ -0,0 +1,433 @@ +package audit + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/dushixiang/pika/internal/protocol" + gopsutilNet "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" +) + +// NetworkAssetsCollector 网络资产收集器 +type NetworkAssetsCollector struct { + config *Config + cache *ProcessCache + executor *CommandExecutor +} + +// NewNetworkAssetsCollector 创建网络资产收集器 +func NewNetworkAssetsCollector(config *Config, cache *ProcessCache, executor *CommandExecutor) *NetworkAssetsCollector { + return &NetworkAssetsCollector{ + config: config, + cache: cache, + executor: executor, + } +} + +// Collect 收集网络资产 +func (nac *NetworkAssetsCollector) Collect() *protocol.NetworkAssets { + assets := &protocol.NetworkAssets{} + + // 收集监听端口 + assets.ListeningPorts = nac.collectListeningPorts() + + // 收集网络连接 + assets.Connections = nac.collectConnections() + + // 收集网卡接口 + assets.Interfaces = nac.collectInterfaces() + + // 收集路由表 + assets.RoutingTable = nac.collectRoutingTable() + + // 收集防火墙规则 + assets.FirewallRules = nac.collectFirewallRules() + + // 收集DNS服务器 + assets.DNSServers = nac.collectDNSServers() + + // 收集ARP表 + assets.ARPTable = nac.collectARPTable() + + // 统计信息 + assets.Statistics = nac.calculateStatistics(assets) + + return assets +} + +// collectListeningPorts 收集监听端口 +func (nac *NetworkAssetsCollector) collectListeningPorts() []protocol.ListeningPort { + var ports []protocol.ListeningPort + + connections, err := gopsutilNet.Connections("all") + if err != nil { + globalLogger.Warn("获取网络连接失败: %v", err) + return ports + } + + for _, conn := range connections { + if conn.Status != "LISTEN" { + continue + } + + // 确定协议类型 + protocolType := "tcp" + if conn.Type == 2 { // SOCK_DGRAM + protocolType = "udp" + } + + port := protocol.ListeningPort{ + Protocol: protocolType, + Address: conn.Laddr.IP, + Port: conn.Laddr.Port, + ProcessPID: conn.Pid, + } + + // 判断是否公网监听 + port.IsPublic = !isLocalIP(conn.Laddr.IP) + + // 获取进程信息 + if conn.Pid > 0 { + if proc, err := process.NewProcess(conn.Pid); err == nil { + port.ProcessName, _ = proc.Name() + port.ProcessPath, _ = proc.Exe() + } + } + + ports = append(ports, port) + } + + return ports +} + +// collectConnections 收集网络连接 +func (nac *NetworkAssetsCollector) collectConnections() []protocol.NetworkConnection { + var connections []protocol.NetworkConnection + + conns, err := gopsutilNet.Connections("all") + if err != nil { + globalLogger.Warn("获取网络连接失败: %v", err) + return connections + } + + // 限制连接数量,避免数据过大 + maxConnections := 200 + establishedCount := 0 + + for _, conn := range conns { + // 只收集 ESTABLISHED 连接 + if conn.Status != "ESTABLISHED" { + continue + } + + if establishedCount >= maxConnections { + break + } + + // 确定协议类型 + protocolType := "tcp" + if conn.Type == 2 { // SOCK_DGRAM + protocolType = "udp" + } + + connection := protocol.NetworkConnection{ + Protocol: protocolType, + LocalAddr: conn.Laddr.IP, + LocalPort: conn.Laddr.Port, + RemoteAddr: conn.Raddr.IP, + RemotePort: conn.Raddr.Port, + State: conn.Status, + ProcessPID: conn.Pid, + } + + // 获取进程信息 + if conn.Pid > 0 { + if proc, err := process.NewProcess(conn.Pid); err == nil { + connection.ProcessName, _ = proc.Name() + } + } + + connections = append(connections, connection) + establishedCount++ + } + + return connections +} + +// collectInterfaces 收集网卡接口 +func (nac *NetworkAssetsCollector) collectInterfaces() []protocol.NetworkInterface { + var interfaces []protocol.NetworkInterface + + ifaces, err := gopsutilNet.Interfaces() + if err != nil { + globalLogger.Warn("获取网卡接口失败: %v", err) + return interfaces + } + + for _, iface := range ifaces { + netInterface := protocol.NetworkInterface{ + Name: iface.Name, + MacAddress: iface.HardwareAddr, + MTU: iface.MTU, + } + + // 提取IP地址 + for _, addr := range iface.Addrs { + netInterface.Addresses = append(netInterface.Addresses, addr.Addr) + } + + // 解析标志 + for _, flag := range iface.Flags { + netInterface.Flags = append(netInterface.Flags, flag) + if flag == "up" { + netInterface.IsUp = true + } + } + + interfaces = append(interfaces, netInterface) + } + + return interfaces +} + +// collectRoutingTable 收集路由表 +func (nac *NetworkAssetsCollector) collectRoutingTable() []protocol.RouteEntry { + var routes []protocol.RouteEntry + + // 使用 route -n 命令 + output, err := nac.executor.Execute("route", "-n") + if err != nil { + globalLogger.Debug("获取路由表失败: %v", err) + return routes + } + + lines := strings.Split(output, "\n") + for i, line := range lines { + // 跳过表头 + if i < 2 { + continue + } + + fields := strings.Fields(line) + if len(fields) < 8 { + continue + } + + route := protocol.RouteEntry{ + Destination: fields[0], + Gateway: fields[1], + Genmask: fields[2], + Interface: fields[7], + } + + // 提取 Metric (可能在不同位置,取决于有无Flags) + if len(fields) > 4 { + fmt.Sscanf(fields[4], "%d", &route.Metric) + } + + routes = append(routes, route) + } + + return routes +} + +// collectFirewallRules 收集防火墙规则 +func (nac *NetworkAssetsCollector) collectFirewallRules() *protocol.FirewallInfo { + fwInfo := &protocol.FirewallInfo{} + + // 尝试检测 iptables + if _, err := exec.LookPath("iptables"); err == nil { + fwInfo.Type = "iptables" + fwInfo.Rules = nac.collectIptablesRules() + fwInfo.Status = "active" + return fwInfo + } + + // 尝试检测 ufw + if _, err := exec.LookPath("ufw"); err == nil { + fwInfo.Type = "ufw" + output, _ := nac.executor.Execute("ufw", "status") + if strings.Contains(output, "Status: active") { + fwInfo.Status = "active" + } else { + fwInfo.Status = "inactive" + } + return fwInfo + } + + // 尝试检测 firewalld + if _, err := exec.LookPath("firewall-cmd"); err == nil { + fwInfo.Type = "firewalld" + output, _ := nac.executor.Execute("firewall-cmd", "--state") + if strings.Contains(output, "running") { + fwInfo.Status = "active" + } else { + fwInfo.Status = "inactive" + } + return fwInfo + } + + fwInfo.Type = "none" + fwInfo.Status = "inactive" + return fwInfo +} + +// collectIptablesRules 收集 iptables 规则 +func (nac *NetworkAssetsCollector) collectIptablesRules() []protocol.FirewallRule { + var rules []protocol.FirewallRule + + // 限制规则数量,只收集前100条 + maxRules := 100 + + output, err := nac.executor.Execute("iptables", "-L", "-n") + if err != nil { + return rules + } + + lines := strings.Split(output, "\n") + currentChain := "" + + for _, line := range lines { + line = strings.TrimSpace(line) + + // 解析链名 + if strings.HasPrefix(line, "Chain") { + fields := strings.Fields(line) + if len(fields) >= 2 { + currentChain = fields[1] + } + continue + } + + // 跳过表头 + if strings.HasPrefix(line, "target") || line == "" { + continue + } + + if len(rules) >= maxRules { + break + } + + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + + rule := protocol.FirewallRule{ + Chain: currentChain, + Target: fields[0], + } + + if len(fields) > 1 { + rule.Protocol = fields[1] + } + if len(fields) > 3 { + rule.Source = fields[3] + } + if len(fields) > 4 { + rule.Dest = fields[4] + } + + rules = append(rules, rule) + } + + return rules +} + +// collectDNSServers 收集DNS服务器 +func (nac *NetworkAssetsCollector) collectDNSServers() []string { + var dnsServers []string + + file, err := os.Open("/etc/resolv.conf") + if err != nil { + return dnsServers + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "nameserver") { + fields := strings.Fields(line) + if len(fields) >= 2 { + dnsServers = append(dnsServers, fields[1]) + } + } + } + + return dnsServers +} + +// collectARPTable 收集ARP表 +func (nac *NetworkAssetsCollector) collectARPTable() []protocol.ARPEntry { + var arpTable []protocol.ARPEntry + + // 限制ARP表数量 + maxEntries := 50 + + output, err := nac.executor.Execute("arp", "-n") + if err != nil { + globalLogger.Debug("获取ARP表失败: %v", err) + return arpTable + } + + lines := strings.Split(output, "\n") + for i, line := range lines { + // 跳过表头 + if i == 0 { + continue + } + + if len(arpTable) >= maxEntries { + break + } + + fields := strings.Fields(line) + if len(fields) < 5 { + continue + } + + entry := protocol.ARPEntry{ + IPAddress: fields[0], + MacAddress: fields[2], + Interface: fields[4], + } + + arpTable = append(arpTable, entry) + } + + return arpTable +} + +// calculateStatistics 计算统计信息 +func (nac *NetworkAssetsCollector) calculateStatistics(assets *protocol.NetworkAssets) *protocol.NetworkStatistics { + stats := &protocol.NetworkStatistics{ + InterfaceCount: len(assets.Interfaces), + ConnectionsByState: make(map[string]int), + } + + // 统计监听端口 + stats.TotalListeningPorts = len(assets.ListeningPorts) + for _, port := range assets.ListeningPorts { + if port.IsPublic { + stats.PublicListeningPorts++ + } + } + + // 统计连接 + stats.ActiveConnections = len(assets.Connections) + for _, conn := range assets.Connections { + stats.ConnectionsByState[conn.State]++ + } + + return stats +} + +// isLocalIP 判断是否本地IP +func isLocalIP(ip string) bool { + return ip == "127.0.0.1" || ip == "::1" || ip == "localhost" +} diff --git a/pkg/agent/audit/asset_process.go b/pkg/agent/audit/asset_process.go new file mode 100644 index 0000000..8f8fa3d --- /dev/null +++ b/pkg/agent/audit/asset_process.go @@ -0,0 +1,155 @@ +package audit + +import ( + "sort" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/process" +) + +// ProcessAssetsCollector 进程资产收集器 +type ProcessAssetsCollector struct { + config *Config + cache *ProcessCache +} + +// NewProcessAssetsCollector 创建进程资产收集器 +func NewProcessAssetsCollector(config *Config, cache *ProcessCache) *ProcessAssetsCollector { + return &ProcessAssetsCollector{ + config: config, + cache: cache, + } +} + +// Collect 收集进程资产 +func (pac *ProcessAssetsCollector) Collect() *protocol.ProcessAssets { + assets := &protocol.ProcessAssets{} + + procs, err := pac.cache.Get() + if err != nil { + globalLogger.Warn("获取进程列表失败: %v", err) + return assets + } + + // 收集所有进程信息 + var allProcesses []protocol.ProcessInfo + for _, p := range procs { + procInfo := pac.convertToProcessInfo(p) + if procInfo != nil { + allProcesses = append(allProcesses, *procInfo) + } + } + + // TOP CPU进程 + assets.TopCPUProcesses = pac.getTopProcesses(allProcesses, "cpu", 15) + + // TOP 内存进程 + assets.TopMemoryProcesses = pac.getTopProcesses(allProcesses, "memory", 15) + + // 可选:完整进程列表 (配置控制) + // 暂时不包含完整列表,避免数据过大 + // assets.RunningProcesses = allProcesses + + // 统计信息 + assets.Statistics = pac.calculateStatistics(procs) + + return assets +} + +// convertToProcessInfo 转换为进程信息 +func (pac *ProcessAssetsCollector) convertToProcessInfo(p *process.Process) *protocol.ProcessInfo { + name, err := p.Name() + if err != nil { + return nil + } + + cmdline, _ := p.Cmdline() + exe, _ := p.Exe() + ppid, _ := p.Ppid() + username, _ := p.Username() + cpuPercent, _ := p.CPUPercent() + memPercent, _ := p.MemoryPercent() + status, _ := p.Status() + createTime, _ := p.CreateTime() + + // 获取内存占用(MB) + memInfo, err := p.MemoryInfo() + var memoryMB uint64 + if err == nil && memInfo != nil { + memoryMB = memInfo.RSS / 1024 / 1024 + } + + return &protocol.ProcessInfo{ + PID: p.Pid, + Name: name, + Cmdline: cmdline, + Exe: exe, + PPID: ppid, + Username: username, + CPUPercent: cpuPercent, + MemPercent: memPercent, + MemoryMB: memoryMB, + Status: status[0], // 取第一个状态 + CreateTime: createTime, + } +} + +// getTopProcesses 获取TOP进程 +func (pac *ProcessAssetsCollector) getTopProcesses(processes []protocol.ProcessInfo, sortBy string, limit int) []protocol.ProcessInfo { + // 复制切片避免修改原始数据 + sorted := make([]protocol.ProcessInfo, len(processes)) + copy(sorted, processes) + + // 排序 + if sortBy == "cpu" { + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].CPUPercent > sorted[j].CPUPercent + }) + } else if sortBy == "memory" { + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].MemoryMB > sorted[j].MemoryMB + }) + } + + // 限制数量 + if len(sorted) > limit { + sorted = sorted[:limit] + } + + return sorted +} + +// calculateStatistics 计算统计信息 +func (pac *ProcessAssetsCollector) calculateStatistics(procs []*process.Process) *protocol.ProcessStatistics { + stats := &protocol.ProcessStatistics{ + TotalProcesses: len(procs), + } + + for _, p := range procs { + status, err := p.Status() + if err != nil { + continue + } + + if len(status) == 0 { + continue + } + + switch status[0] { + case "R": + stats.RunningProcesses++ + case "S", "D", "I": + stats.SleepingProcesses++ + case "Z": + stats.ZombieProcesses++ + } + + // 统计线程数 + numThreads, err := p.NumThreads() + if err == nil { + stats.ThreadCount += int(numThreads) + } + } + + return stats +} diff --git a/pkg/agent/audit/asset_user.go b/pkg/agent/audit/asset_user.go new file mode 100644 index 0000000..3f503b8 --- /dev/null +++ b/pkg/agent/audit/asset_user.go @@ -0,0 +1,392 @@ +package audit + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" +) + +// UserAssetsCollector 用户资产收集器 +type UserAssetsCollector struct { + config *Config + executor *CommandExecutor +} + +// NewUserAssetsCollector 创建用户资产收集器 +func NewUserAssetsCollector(config *Config, executor *CommandExecutor) *UserAssetsCollector { + return &UserAssetsCollector{ + config: config, + executor: executor, + } +} + +// Collect 收集用户资产 +func (uac *UserAssetsCollector) Collect() *protocol.UserAssets { + assets := &protocol.UserAssets{} + + // 收集系统用户 + assets.SystemUsers = uac.collectSystemUsers() + + // 收集登录历史 + assets.LoginHistory = uac.collectLoginHistory() + + // 收集当前登录 + assets.CurrentLogins = uac.collectCurrentLogins() + + // 收集SSH密钥 + assets.SSHKeys = uac.collectSSHKeys() + + // 收集Sudo用户 + assets.SudoUsers = uac.collectSudoUsers() + + // 统计信息 + assets.Statistics = uac.calculateStatistics(assets) + + return assets +} + +// collectSystemUsers 收集系统用户 +func (uac *UserAssetsCollector) collectSystemUsers() []protocol.UserInfo { + var users []protocol.UserInfo + + // 读取 /etc/passwd + passwdFile, err := os.Open("/etc/passwd") + if err != nil { + globalLogger.Warn("读取/etc/passwd失败: %v", err) + return users + } + defer passwdFile.Close() + + // 读取 /etc/shadow 获取密码信息 + shadowPasswords := uac.readShadowPasswords() + + scanner := bufio.NewScanner(passwdFile) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) < 7 { + continue + } + + username := parts[0] + uid := parts[2] + gid := parts[3] + homeDir := parts[5] + shell := parts[6] + + // 判断是否可登录 + isLoginable := !strings.Contains(shell, "nologin") && + !strings.Contains(shell, "false") && + shell != "" + + // 判断是否Root等效 + isRootEquiv := uid == "0" && username != "root" + + // 检查是否有密码 + hasPassword := false + if pwd, ok := shadowPasswords[username]; ok { + hasPassword = pwd != "" && pwd != "!" && pwd != "*" + } + + user := protocol.UserInfo{ + Username: username, + UID: uid, + GID: gid, + HomeDir: homeDir, + Shell: shell, + IsLoginable: isLoginable, + IsRootEquiv: isRootEquiv, + HasPassword: hasPassword, + } + + users = append(users, user) + } + + return users +} + +// readShadowPasswords 读取shadow密码 +func (uac *UserAssetsCollector) readShadowPasswords() map[string]string { + passwords := make(map[string]string) + + shadowFile, err := os.Open("/etc/shadow") + if err != nil { + return passwords + } + defer shadowFile.Close() + + scanner := bufio.NewScanner(shadowFile) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) >= 2 { + passwords[parts[0]] = parts[1] + } + } + + return passwords +} + +// collectLoginHistory 收集登录历史 +func (uac *UserAssetsCollector) collectLoginHistory() []protocol.LoginRecord { + var records []protocol.LoginRecord + + // 使用 last 命令获取登录历史 + output, err := uac.executor.Execute("last", "-n", "50", "-F") + if err != nil { + globalLogger.Debug("获取登录历史失败: %v", err) + return records + } + + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "wtmp") || strings.HasPrefix(line, "reboot") { + continue + } + + fields := strings.Fields(line) + if len(fields) < 7 { + continue + } + + username := fields[0] + terminal := fields[1] + ip := fields[2] + + // 解析时间 (简化处理,使用当前时间作为近似) + timestamp := time.Now().UnixMilli() + + record := protocol.LoginRecord{ + Username: username, + Terminal: terminal, + IP: ip, + Timestamp: timestamp, + Status: "success", + } + + records = append(records, record) + + // 限制数量 + if len(records) >= 50 { + break + } + } + + return records +} + +// collectCurrentLogins 收集当前登录 +func (uac *UserAssetsCollector) collectCurrentLogins() []protocol.LoginSession { + var sessions []protocol.LoginSession + + // 使用 w 命令 + output, err := uac.executor.Execute("w", "-h") + if err != nil { + globalLogger.Debug("获取当前登录失败: %v", err) + return sessions + } + + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + fields := strings.Fields(line) + if len(fields) < 4 { + continue + } + + username := fields[0] + terminal := fields[1] + ip := fields[2] + + // 解析空闲时间 + idleStr := fields[3] + idleTime := 0 + if idleStr != "-" { + // 简化处理,不解析复杂格式 + fmt.Sscanf(idleStr, "%d", &idleTime) + } + + session := protocol.LoginSession{ + Username: username, + Terminal: terminal, + IP: ip, + LoginTime: time.Now().UnixMilli(), + IdleTime: idleTime, + } + + sessions = append(sessions, session) + } + + return sessions +} + +// collectSSHKeys 收集SSH密钥 +func (uac *UserAssetsCollector) collectSSHKeys() []protocol.SSHKeyInfo { + var keys []protocol.SSHKeyInfo + + // 获取所有用户目录 + userDirs := uac.getAllUserDirectories() + + for _, userDir := range userDirs { + keyPath := filepath.Join(userDir, ".ssh", "authorized_keys") + + info, err := os.Stat(keyPath) + if err != nil { + continue + } + + // 读取密钥文件 + file, err := os.Open(keyPath) + if err != nil { + continue + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + + keyType := parts[0] + comment := "" + if len(parts) > 2 { + comment = parts[2] + } + + // 计算指纹 (简化,只取前16个字符) + fingerprint := "" + if len(parts[1]) > 16 { + fingerprint = parts[1][:16] + "..." + } else { + fingerprint = parts[1] + } + + keyInfo := protocol.SSHKeyInfo{ + Username: filepath.Base(filepath.Dir(filepath.Dir(keyPath))), + KeyType: keyType, + Fingerprint: fingerprint, + Comment: comment, + FilePath: keyPath, + AddedTime: info.ModTime().UnixMilli(), + } + + keys = append(keys, keyInfo) + } + file.Close() + } + + return keys +} + +// collectSudoUsers 收集Sudo用户 +func (uac *UserAssetsCollector) collectSudoUsers() []protocol.SudoUserInfo { + var sudoUsers []protocol.SudoUserInfo + + // 读取 /etc/sudoers + sudoersFile, err := os.Open("/etc/sudoers") + if err != nil { + globalLogger.Debug("读取sudoers失败: %v", err) + return sudoUsers + } + defer sudoersFile.Close() + + scanner := bufio.NewScanner(sudoersFile) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 简单匹配包含 ALL 的行 + if !strings.Contains(line, "ALL") { + continue + } + + fields := strings.Fields(line) + if len(fields) < 1 { + continue + } + + username := fields[0] + if strings.HasPrefix(username, "%") { + // 组,跳过 + continue + } + + sudoUser := protocol.SudoUserInfo{ + Username: username, + Rules: line, + NoPasswd: strings.Contains(line, "NOPASSWD"), + } + + sudoUsers = append(sudoUsers, sudoUser) + } + + return sudoUsers +} + +// getAllUserDirectories 获取所有用户目录 +func (uac *UserAssetsCollector) getAllUserDirectories() []string { + var dirs []string + + // 从 /etc/passwd 读取 + file, err := os.Open("/etc/passwd") + if err != nil { + return dirs + } + defer file.Close() + + seen := make(map[string]bool) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + parts := strings.Split(scanner.Text(), ":") + if len(parts) >= 6 { + homeDir := parts[5] + if homeDir != "" && !seen[homeDir] { + dirs = append(dirs, homeDir) + seen[homeDir] = true + } + } + } + + return dirs +} + +// calculateStatistics 计算统计信息 +func (uac *UserAssetsCollector) calculateStatistics(assets *protocol.UserAssets) *protocol.UserStatistics { + stats := &protocol.UserStatistics{ + TotalUsers: len(assets.SystemUsers), + RecentLoginCount: len(assets.LoginHistory), + } + + for _, user := range assets.SystemUsers { + if user.IsLoginable { + stats.LoginableUsers++ + } + if user.IsRootEquiv { + stats.RootEquivalentUsers++ + } + } + + // 统计失败登录 (可以从 /var/log/auth.log 读取,这里简化) + stats.FailedLoginCount = 0 + + return stats +} diff --git a/pkg/agent/audit/audit.go b/pkg/agent/audit/audit.go index 3b75c27..ba30152 100644 --- a/pkg/agent/audit/audit.go +++ b/pkg/agent/audit/audit.go @@ -17,20 +17,18 @@ const ( StatusSkip = "skip" ) -// Auditor VPS 安全审计器 +// Auditor VPS 资产收集器(Agent端只负责信息收集) type Auditor struct { config *Config cache *ProcessCache executor *CommandExecutor - evidence *EvidenceCollector - // 各个检查器 - rootkitChecker *RootkitChecker - processChecker *ProcessChecker - sshChecker *SSHChecker - networkChecker *NetworkChecker - fileChecker *FileChecker - accountChecker *AccountChecker + // 资产收集器 + networkAssetsCollector *NetworkAssetsCollector + processAssetsCollector *ProcessAssetsCollector + userAssetsCollector *UserAssetsCollector + fileAssetsCollector *FileAssetsCollector + kernelAssetsCollector *KernelAssetsCollector } // NewAuditor 创建审计器 @@ -42,25 +40,22 @@ func NewAuditor(config *Config) *Auditor { // 初始化共享组件 cache := NewProcessCache(config.PerformanceConfig.ProcessCacheDuration) executor := NewCommandExecutor(config.PerformanceConfig.CommandTimeout) - evidence := NewEvidenceCollector() - // 初始化各个检查器 + // 初始化资产收集器 return &Auditor{ config: config, cache: cache, executor: executor, - evidence: evidence, - rootkitChecker: NewRootkitChecker(config, cache, evidence), - processChecker: NewProcessChecker(config, cache, evidence), - sshChecker: NewSSHChecker(config, cache, evidence, executor), - networkChecker: NewNetworkChecker(config, cache, evidence), - fileChecker: NewFileChecker(config, evidence, executor), - accountChecker: NewAccountChecker(config), + networkAssetsCollector: NewNetworkAssetsCollector(config, cache, executor), + processAssetsCollector: NewProcessAssetsCollector(config, cache), + userAssetsCollector: NewUserAssetsCollector(config, executor), + fileAssetsCollector: NewFileAssetsCollector(config, executor), + kernelAssetsCollector: NewKernelAssetsCollector(config, executor), } } -// RunAudit 执行 VPS 安全审计 +// RunAudit 执行 VPS 资产收集(Agent端只收集信息,不做安全判断) func (a *Auditor) RunAudit() (*protocol.VPSAuditResult, error) { startTime := time.Now().UnixMilli() @@ -71,10 +66,10 @@ func (a *Auditor) RunAudit() (*protocol.VPSAuditResult, error) { // 检查运行权限 if os.Geteuid() != 0 { - return nil, fmt.Errorf("需要root权限运行完整审计") + return nil, fmt.Errorf("需要root权限运行完整收集") } - globalLogger.Info("开始安全审计...") + globalLogger.Info("开始资产收集...") // 获取系统信息 sysInfoCollector := NewSystemInfoCollector(a.executor) @@ -83,126 +78,105 @@ func (a *Auditor) RunAudit() (*protocol.VPSAuditResult, error) { return nil, fmt.Errorf("获取系统信息失败: %w", err) } - // 收集警告 + // 收集警告(仅收集过程中的错误或异常) warningCollector := NewWarningCollector() - // 执行所有安全检查 - var securityChecks []protocol.SecurityCheck + // 收集资产清单 + globalLogger.Info("开始收集资产清单...") + assetInventory := a.collectAssets() - checks := []struct { - name string - fn func() protocol.SecurityCheck - }{ - {"Rootkit检测", a.rootkitChecker.Check}, - {"可疑进程检测", a.processChecker.CheckSuspiciousProcesses}, - {"进程环境变量检查", a.processChecker.CheckSuspiciousEnvVars}, - {"SSH安全检查", a.sshChecker.Check}, - {"端口监听检查", a.networkChecker.CheckListeningPorts}, - {"网络连接检查", a.networkChecker.CheckNetworkConnections}, - {"可疑文件检查", a.fileChecker.CheckSuspiciousFiles}, - {"文件完整性检查", a.fileChecker.CheckFileIntegrity}, - {"不可变文件检查", a.fileChecker.CheckImmutableFiles}, - {"系统账户检查", a.accountChecker.CheckSystemAccounts}, - } - - for _, check := range checks { - globalLogger.Debug("执行检查: %s", check.name) - result := check.fn() - securityChecks = append(securityChecks, result) - - // 收集警告 - for _, detail := range result.Details { - if detail.Status == StatusSkip && detail.Message != "" { - warningCollector.Add(fmt.Sprintf("[%s] %s", result.Category, detail.Message)) - } - } - } + // 计算统计信息 + statistics := a.calculateStatistics(assetInventory) endTime := time.Now().UnixMilli() - // 计算风险评分(使用改进的算法) - riskScore, threatLevel, recommendations := a.calculateRiskScore(securityChecks) + globalLogger.Info("资产收集完成,耗时 %dms", endTime-startTime) - globalLogger.Info("审计完成,耗时 %dms, 风险评分: %d, 威胁等级: %s", - endTime-startTime, riskScore, threatLevel) - - return &protocol.VPSAuditResult{ + result := &protocol.VPSAuditResult{ SystemInfo: *systemInfo, - SecurityChecks: securityChecks, StartTime: startTime, EndTime: endTime, - RiskScore: riskScore, - ThreatLevel: threatLevel, - Recommendations: recommendations, - AuditWarnings: warningCollector.GetAll(), - }, nil + CollectWarnings: warningCollector.GetAll(), + } + + // 设置资产清单和统计信息 + if assetInventory != nil { + result.AssetInventory = *assetInventory + } + if statistics != nil { + result.Statistics = *statistics + } + + return result, nil } -// calculateRiskScore 计算综合风险评分(改进版) -func (a *Auditor) calculateRiskScore(checks []protocol.SecurityCheck) (int, string, []string) { - score := 0 - var recommendations []string +// collectAssets 收集资产清单 +func (a *Auditor) collectAssets() *protocol.AssetInventory { + inventory := &protocol.AssetInventory{} - // 使用配置的权重 - for _, check := range checks { - weight, exists := a.config.ScoringConfig.Weights[check.Category] - if !exists { - // 默认权重 - weight = CheckWeight{ - Category: check.Category, - FailScore: 20, - WarnScore: 5, - } - globalLogger.Warn("检查类别 '%s' 没有配置权重,使用默认值", check.Category) - } - - for _, detail := range check.Details { - switch detail.Status { - case StatusFail: - score += weight.FailScore - recommendations = append(recommendations, - fmt.Sprintf("【紧急】%s: %s", check.Category, detail.Message)) - case StatusWarn: - score += weight.WarnScore - recommendations = append(recommendations, - fmt.Sprintf("【警告】%s: %s", check.Category, detail.Message)) - } - } + // 并发收集各类资产 + type assetTask struct { + name string + fn func() } - // 限制最大分数为 100 - if score > 100 { - score = 100 + tasks := []assetTask{ + {"网络资产", func() { + inventory.NetworkAssets = a.networkAssetsCollector.Collect() + }}, + {"进程资产", func() { + inventory.ProcessAssets = a.processAssetsCollector.Collect() + }}, + {"用户资产", func() { + inventory.UserAssets = a.userAssetsCollector.Collect() + }}, + {"文件资产", func() { + inventory.FileAssets = a.fileAssetsCollector.Collect() + }}, + {"内核资产", func() { + inventory.KernelAssets = a.kernelAssetsCollector.Collect() + }}, } - // 确定威胁等级(使用配置的阈值) - var level string - switch { - case score >= a.config.ScoringConfig.CriticalThreshold: - level = "critical" - case score >= a.config.ScoringConfig.HighThreshold: - level = "high" - case score >= a.config.ScoringConfig.MediumThreshold: - level = "medium" - default: - level = "low" + // 串行执行(避免资源竞争,实际可以优化为并发) + for _, task := range tasks { + globalLogger.Debug("收集%s...", task.name) + task.fn() } - // 限制建议数量 - if len(recommendations) > a.config.ScoringConfig.MaxRecommendations { - recommendations = recommendations[:a.config.ScoringConfig.MaxRecommendations] - } - - return score, level, recommendations + return inventory } -// RunAuditWithConfig 使用自定义配置执行审计 +// calculateStatistics 计算统计信息 +func (a *Auditor) calculateStatistics(inventory *protocol.AssetInventory) *protocol.AuditStatistics { + stats := &protocol.AuditStatistics{} + + if inventory.NetworkAssets != nil { + stats.NetworkStats = inventory.NetworkAssets.Statistics + } + + if inventory.ProcessAssets != nil { + stats.ProcessStats = inventory.ProcessAssets.Statistics + } + + if inventory.UserAssets != nil { + stats.UserStats = inventory.UserAssets.Statistics + } + + if inventory.FileAssets != nil { + stats.FileStats = inventory.FileAssets.Statistics + } + + return stats +} + +// RunAuditWithConfig 使用自定义配置执行资产收集 func RunAuditWithConfig(config *Config) (*protocol.VPSAuditResult, error) { auditor := NewAuditor(config) return auditor.RunAudit() } -// RunAudit 使用默认配置执行审计(保持向后兼容) +// RunAudit 使用默认配置执行资产收集(保持向后兼容) func RunAudit() (*protocol.VPSAuditResult, error) { return RunAuditWithConfig(nil) } diff --git a/pkg/agent/audit/check_misc.go b/pkg/agent/audit/check_misc.go deleted file mode 100644 index df94bb6..0000000 --- a/pkg/agent/audit/check_misc.go +++ /dev/null @@ -1,419 +0,0 @@ -package audit - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/dushixiang/pika/internal/protocol" -) - -// 这个文件包含文件检查、账户检查、Cron 检查和登录历史检查 -// 为简化重构,将这些相对独立的检查整合在一起 - -// FileChecker 文件检查器 -type FileChecker struct { - config *Config - evidence *EvidenceCollector - executor *CommandExecutor -} - -// NewFileChecker 创建文件检查器 -func NewFileChecker(config *Config, evidence *EvidenceCollector, executor *CommandExecutor) *FileChecker { - return &FileChecker{ - config: config, - evidence: evidence, - executor: executor, - } -} - -// CheckSuspiciousFiles 检查可疑文件 -func (fc *FileChecker) CheckSuspiciousFiles() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "suspicious_files", - Status: StatusPass, - Message: "可疑文件检查", - Details: []protocol.SecurityCheckSub{}, - } - - suspiciousCount := 0 - - // 检查临时目录下的可执行文件 - for _, dir := range fc.config.FileConfig.TempDirs { - files := fc.findSuspiciousExecutables(dir) - if len(files) > 0 { - check.Status = StatusWarn - for _, file := range files { - suspiciousCount++ - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "tmp_executable", - Status: StatusWarn, - Message: file, - }) - } - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "files_clean", - Status: StatusPass, - Message: "未发现可疑文件", - }) - check.Message = "文件系统正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑文件", suspiciousCount) - } - - return check -} - -// CheckFileIntegrity 检查系统文件完整性(修复bug版本) -func (fc *FileChecker) CheckFileIntegrity() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "file_integrity", - Status: StatusPass, - Message: "系统文件完整性检查", - Details: []protocol.SecurityCheckSub{}, - } - - suspiciousCount := 0 - - // 1. 检查 /etc/ld.so.preload - if content, err := os.ReadFile("/etc/ld.so.preload"); err == nil { - contentStr := strings.TrimSpace(string(content)) - if contentStr != "" { - suspiciousCount++ - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ld_preload_exists", - Status: StatusFail, - Message: fmt.Sprintf("发现动态链接库劫持配置 (/etc/ld.so.preload): %s", contentStr), - Evidence: &protocol.Evidence{ - FilePath: "/etc/ld.so.preload", - RiskLevel: "critical", - }, - }) - } - } - - // 2. 检查系统文件最近修改时间 - recentModifiedCount := 0 - for _, binPath := range fc.config.FileConfig.CriticalBinaries { - if _, err := os.Stat(binPath); err != nil { - continue - } - - isModified, verifyOutput := fc.verifyFileByModTime(binPath) - if isModified { - recentModifiedCount++ - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "recently_modified_binary", - Status: StatusWarn, - Message: verifyOutput, - }) - } - } - - if suspiciousCount > 0 || recentModifiedCount > 0 { - check.Message = fmt.Sprintf("发现 %d 个文件完整性异常", suspiciousCount+recentModifiedCount) - } else { - check.Message = "核心文件完整性正常" - } - - return check -} - -// CheckImmutableFiles 检查不可变文件 -func (fc *FileChecker) CheckImmutableFiles() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "immutable_files", - Status: StatusPass, - Message: "不可变文件检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 检查 lsattr 是否可用 - if _, err := exec.LookPath("lsattr"); err != nil { - check.Status = StatusSkip - check.Message = "lsattr 命令不可用,无法检查不可变属性" - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "lsattr_unavailable", - Status: StatusSkip, - Message: "系统未安装 lsattr 工具(需要 e2fsprogs 包)", - }) - return check - } - - suspiciousCount := 0 - - for _, file := range fc.config.FileConfig.ImmutableCheckFiles { - if _, err := os.Stat(file); err != nil { - continue - } - - cmd := exec.Command("lsattr", "-d", file) - output, err := cmd.Output() - if err != nil { - continue - } - - outStr := string(output) - if strings.Contains(outStr, "-i-") || strings.Contains(outStr, "----i") { - suspiciousCount++ - check.Status = StatusFail - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "immutable_file_found", - Status: StatusFail, - Message: fmt.Sprintf("关键文件被锁定(不可变): %s (可能是Rootkit保护)", file), - Evidence: &protocol.Evidence{ - FilePath: file, - RiskLevel: "high", - }, - }) - } - } - - if suspiciousCount > 0 { - check.Message = fmt.Sprintf("发现 %d 个异常锁定的系统文件", suspiciousCount) - } else { - check.Message = "未发现被恶意锁定的文件" - } - - return check -} - -// findSuspiciousExecutables 查找可疑的可执行文件 -func (fc *FileChecker) findSuspiciousExecutables(dir string) []string { - var suspicious []string - - entries, err := os.ReadDir(dir) - if err != nil { - return suspicious - } - - now := time.Now() - for _, entry := range entries { - if entry.IsDir() { - continue - } - - fullPath := filepath.Join(dir, entry.Name()) - info, err := entry.Info() - if err != nil { - continue - } - - // 检查是否可执行 - if info.Mode()&0111 != 0 { - age := now.Sub(info.ModTime()) - sizeKB := info.Size() / 1024 - sizeMB := info.Size() / 1024 / 1024 - - if age < time.Duration(fc.config.FileConfig.RecentExecutableHours)*time.Hour { - suspicious = append(suspicious, fmt.Sprintf("%s 下的最近可执行文件: %s (大小: %dKB, 创建时间: %s)", - dir, fullPath, sizeKB, info.ModTime().Format("2006-01-02 15:04:05"))) - } else if sizeMB > fc.config.FileConfig.LargeExecutableMB { - suspicious = append(suspicious, fmt.Sprintf("%s 下的大型可执行文件: %s (大小: %dMB)", - dir, fullPath, sizeMB)) - } - } - } - - return suspicious -} - -// verifyFileByModTime 通过修改时间检查文件 -func (fc *FileChecker) verifyFileByModTime(filePath string) (bool, string) { - info, err := os.Stat(filePath) - if err != nil { - return false, "" - } - - modTime := info.ModTime() - age := time.Since(modTime) - - // 如果在最近 24 小时内被修改,标记为可疑 - if age < 24*time.Hour { - return true, fmt.Sprintf("系统命令最近24小时内有更新: %s (请确认是否为正常运维)", filePath) - } - - return false, "" -} - -// AccountChecker 账户检查器 -type AccountChecker struct { - config *Config -} - -// NewAccountChecker 创建账户检查器 -func NewAccountChecker(config *Config) *AccountChecker { - return &AccountChecker{config: config} -} - -// CheckSystemAccounts 检查系统账户异常 -func (ac *AccountChecker) CheckSystemAccounts() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "system_accounts", - Status: StatusPass, - Message: "系统账户检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 1. 检查可疑账户 - suspiciousAccounts := ac.findSuspiciousAccounts() - for _, acc := range suspiciousAccounts { - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_account", - Status: StatusWarn, - Message: acc, - }) - } - - // 2. 检查无密码账户 - noPasswordAccounts := ac.findNoPasswordAccounts() - for _, acc := range noPasswordAccounts { - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "no_password_account", - Status: StatusFail, - Message: fmt.Sprintf("无密码账户: %s", acc), - }) - } - - // 3. 检查 UID 为 0 的非 root 账户 - rootUIDAccounts := ac.findRootUIDAccounts() - for _, acc := range rootUIDAccounts { - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "uid0_account", - Status: StatusFail, - Message: fmt.Sprintf("UID为0的非root账户: %s", acc), - }) - } - - if check.Status == StatusPass { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "accounts_clean", - Status: StatusPass, - Message: "系统账户正常", - }) - check.Message = "账户配置正常" - } else if check.Status == StatusWarn { - check.Message = "发现可疑账户" - } else { - check.Message = "发现严重账户安全问题" - } - - return check -} - -// findSuspiciousAccounts 查找可疑账户 -func (ac *AccountChecker) findSuspiciousAccounts() []string { - var suspicious []string - - file, err := os.Open("/etc/passwd") - if err != nil { - return suspicious - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, ":") - if len(parts) < 7 { - continue - } - - username := parts[0] - uidStr := parts[2] - shell := parts[6] - - uid, _ := strconv.Atoi(uidStr) - - // 系统账户(UID<1000)但有可登录 shell - if uid < 1000 && username != "root" { - legitimateShells := []string{ - "/sbin/nologin", "/bin/false", "/usr/sbin/nologin", - "/bin/sync", "/sbin/shutdown", "/sbin/halt", - } - - isLegitimate := false - for _, legitShell := range legitimateShells { - if shell == legitShell { - isLegitimate = true - break - } - } - - if !isLegitimate { - suspicious = append(suspicious, fmt.Sprintf("系统账户有可登录shell: %s (UID: %s, Shell: %s)", username, uidStr, shell)) - } - } - } - - return suspicious -} - -// findNoPasswordAccounts 查找无密码账户 -func (ac *AccountChecker) findNoPasswordAccounts() []string { - var noPassword []string - - file, err := os.Open("/etc/shadow") - if err != nil { - return noPassword - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, ":") - if len(parts) >= 2 { - username := parts[0] - password := parts[1] - - if password == "" { - noPassword = append(noPassword, username) - } - } - } - - return noPassword -} - -// findRootUIDAccounts 查找 UID 为 0 的非 root 账户 -func (ac *AccountChecker) findRootUIDAccounts() []string { - var rootUID []string - - file, err := os.Open("/etc/passwd") - if err != nil { - return rootUID - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, ":") - if len(parts) >= 3 { - username := parts[0] - uid := parts[2] - - if uid == "0" && username != "root" { - rootUID = append(rootUID, username) - } - } - } - - return rootUID -} diff --git a/pkg/agent/audit/check_network.go b/pkg/agent/audit/check_network.go deleted file mode 100644 index 3c24fbb..0000000 --- a/pkg/agent/audit/check_network.go +++ /dev/null @@ -1,270 +0,0 @@ -package audit - -import ( - "fmt" - "strings" - - "github.com/dushixiang/pika/internal/protocol" - gopsutilNet "github.com/shirou/gopsutil/v4/net" - "github.com/shirou/gopsutil/v4/process" -) - -// NetworkChecker 网络检查器 -type NetworkChecker struct { - config *Config - cache *ProcessCache - evidence *EvidenceCollector - strUtil *StringUtils -} - -// NewNetworkChecker 创建网络检查器 -func NewNetworkChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector) *NetworkChecker { - return &NetworkChecker{ - config: config, - cache: cache, - evidence: evidence, - strUtil: &StringUtils{}, - } -} - -// CheckListeningPorts 检查异常监听端口 -func (nc *NetworkChecker) CheckListeningPorts() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "listening_ports", - Status: StatusPass, - Message: "端口监听检查", - Details: []protocol.SecurityCheckSub{}, - } - - connections, err := gopsutilNet.Connections("all") - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取网络连接信息" - globalLogger.Error("无法获取网络连接信息: %v", err) - return check - } - - suspiciousCount := 0 - totalListening := 0 - publicListening := 0 - - for _, conn := range connections { - if conn.Status != "LISTEN" { - continue - } - - totalListening++ - port := conn.Laddr.Port - listenAddr := conn.Laddr.IP - pid := conn.Pid - - // 本地回环监听的服务一般是安全的,跳过 - if listenAddr == "127.0.0.1" || listenAddr == "::1" { - continue - } - - publicListening++ - - // 获取进程信息 - if pid <= 0 { - continue - } - - proc, err := process.NewProcess(int32(pid)) - if err != nil { - continue - } - - name, _ := proc.Name() - exe, _ := proc.Exe() - cmdline, _ := proc.Cmdline() - - // 检查 1: 高危端口 - if desc, exists := nc.config.NetworkConfig.SuspiciousPorts[port]; exists { - suspiciousCount++ - check.Status = StatusWarn - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("suspicious_port_%d", port), - Status: StatusWarn, - Message: fmt.Sprintf("高危端口监听: %s:%d (%s) - 进程: %s (PID: %d)", listenAddr, port, desc, name, pid), - Evidence: nc.evidence.CollectProcessEvidence(proc, "high"), - }) - continue - } - - // 检查 2: 挖矿程序监听端口 - if nc.isMinerProcess(name, cmdline) { - suspiciousCount++ - check.Status = StatusFail - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("miner_listener_%d", port), - Status: StatusFail, - Message: fmt.Sprintf("挖矿程序监听端口: %s 监听 %s:%d (PID: %d)", name, listenAddr, port, pid), - Evidence: nc.evidence.CollectProcessEvidence(proc, "high"), - }) - continue - } - - // 检查 3: 可疑路径的程序监听端口 - if nc.isSuspiciousPath(exe) { - suspiciousCount++ - check.Status = StatusFail - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("suspicious_exe_listener_%d", port), - Status: StatusFail, - Message: fmt.Sprintf("可疑路径程序监听端口: %s 监听 %s:%d (PID: %d, 路径: %s)", name, listenAddr, port, pid, exe), - Evidence: nc.evidence.CollectProcessEvidence(proc, "high"), - }) - continue - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "safe_ports", - Status: StatusPass, - Message: fmt.Sprintf("监听%d个端口(公网%d个),未发现可疑监听", totalListening, publicListening), - }) - check.Message = "端口监听正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑端口监听", suspiciousCount) - } - - return check -} - -// CheckNetworkConnections 检查异常网络连接 -func (nc *NetworkChecker) CheckNetworkConnections() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "network_connections", - Status: StatusPass, - Message: "网络连接检查", - Details: []protocol.SecurityCheckSub{}, - } - - connections, err := gopsutilNet.Connections("all") - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取网络连接" - globalLogger.Error("无法获取网络连接: %v", err) - return check - } - - suspiciousCount := 0 - establishedCount := 0 - - for _, conn := range connections { - if conn.Status != "ESTABLISHED" { - continue - } - - establishedCount++ - - // 获取进程信息 - var processName string - var processPath string - if conn.Pid > 0 { - proc, err := process.NewProcess(int32(conn.Pid)) - if err == nil { - processName, _ = proc.Name() - processPath, _ = proc.Exe() - } - } - - // 检查 1: 连接到挖矿池 - if nc.isMiningPool(conn.Raddr.IP, conn.Raddr.Port) { - suspiciousCount++ - check.Status = StatusFail - - msg := fmt.Sprintf("连接到挖矿池: %s:%d", conn.Raddr.IP, conn.Raddr.Port) - if processName != "" { - msg += fmt.Sprintf(" (进程: %s, PID: %d)", processName, conn.Pid) - } - - var evidence *protocol.Evidence - if conn.Pid > 0 { - proc, _ := process.NewProcess(int32(conn.Pid)) - if proc != nil { - evidence = nc.evidence.CollectProcessEvidence(proc, "high") - } - } - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "mining_connection", - Status: StatusFail, - Message: msg, - Evidence: evidence, - }) - } - - // 检查 2: 可疑路径的进程外连 - if processPath != "" && nc.isSuspiciousPath(processPath) { - suspiciousCount++ - check.Status = StatusFail - - var evidence *protocol.Evidence - if conn.Pid > 0 { - proc, _ := process.NewProcess(int32(conn.Pid)) - if proc != nil { - evidence = nc.evidence.CollectProcessEvidence(proc, "high") - } - } - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_path_connection", - Status: StatusFail, - Message: fmt.Sprintf("可疑路径进程外连: %s (PID:%d) -> %s:%d", processPath, conn.Pid, conn.Raddr.IP, conn.Raddr.Port), - Evidence: evidence, - }) - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "connections_clean", - Status: StatusPass, - Message: fmt.Sprintf("共 %d 个活动连接,未发现异常", establishedCount), - }) - check.Message = "网络连接正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑连接", suspiciousCount) - } - - return check -} - -// isSuspiciousPath 检查可执行文件路径是否可疑 -func (nc *NetworkChecker) isSuspiciousPath(path string) bool { - if path == "" { - return false - } - - for _, sp := range nc.config.FileConfig.SuspiciousPaths { - if strings.Contains(path, sp) { - return true - } - } - - return false -} - -// isMinerProcess 检查是否是挖矿进程 -func (nc *NetworkChecker) isMinerProcess(name, cmdline string) bool { - return nc.strUtil.ContainsAny(name+" "+cmdline, nc.config.ProcessConfig.MinerKeywords) -} - -// isMiningPool 检查是否连接到挖矿池 -func (nc *NetworkChecker) isMiningPool(ip string, port uint32) bool { - // 检查端口 - for _, p := range nc.config.NetworkConfig.MinerPorts { - if port == p { - return true - } - } - - // TODO: 可以添加域名检查(需要反向 DNS) - return false -} diff --git a/pkg/agent/audit/check_process.go b/pkg/agent/audit/check_process.go deleted file mode 100644 index f74701f..0000000 --- a/pkg/agent/audit/check_process.go +++ /dev/null @@ -1,513 +0,0 @@ -package audit - -import ( - "fmt" - "strings" - "time" - - "github.com/dushixiang/pika/internal/protocol" - "github.com/shirou/gopsutil/v4/process" -) - -// ProcessChecker 进程检查器 -type ProcessChecker struct { - config *Config - cache *ProcessCache - evidence *EvidenceCollector -} - -// NewProcessChecker 创建进程检查器 -func NewProcessChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector) *ProcessChecker { - return &ProcessChecker{ - config: config, - cache: cache, - evidence: evidence, - } -} - -// CheckSuspiciousProcesses 检查可疑进程 -func (pc *ProcessChecker) CheckSuspiciousProcesses() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "suspicious_processes", - Status: StatusPass, - Message: "可疑进程检测", - Details: []protocol.SecurityCheckSub{}, - } - - procs, err := pc.cache.Get() - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取进程列表" - globalLogger.Error("无法获取进程列表: %v", err) - return check - } - - suspiciousCount := 0 - - for _, p := range procs { - name, _ := p.Name() - cmdline, _ := p.Cmdline() - exe, _ := p.Exe() - - // 1. 检查挖矿 - if pc.isMinerProcess(name, cmdline) { - suspiciousCount++ - check.Status = StatusFail - - cpuPercent, _ := p.CPUPercent() - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "miner_process", - Status: StatusFail, - Message: fmt.Sprintf("检测到挖矿程序: %s (PID: %d, CPU: %.1f%%)", name, p.Pid, cpuPercent), - Evidence: pc.evidence.CollectProcessEvidence(p, "high"), - }) - continue - } - - // 2. 检查高 CPU + 网络连接的可疑进程 - if pc.isHighCPUMinerSuspect(p, name, exe) { - suspiciousCount++ - check.Status = StatusWarn - - cpuPercent, _ := p.CPUPercent() - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "high_cpu_network_process", - Status: StatusWarn, - Message: fmt.Sprintf("高CPU进程持续网络连接: %s (PID: %d, CPU: %.1f%%) - 疑似挖矿", name, p.Pid, cpuPercent), - Evidence: pc.evidence.CollectProcessEvidence(p, "medium"), - }) - continue - } - - // 3. 检查反弹 Shell - if pc.isReverseShell(cmdline) { - suspiciousCount++ - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "reverse_shell", - Status: StatusFail, - Message: fmt.Sprintf("检测到反弹shell: %s (PID: %d)", cmdline, p.Pid), - Evidence: pc.evidence.CollectProcessEvidence(p, "high"), - }) - continue - } - - // 4. 检查无文件进程/被删除的二进制 - if strings.Contains(exe, "(deleted)") { - // 检查白名单 - if pc.isInDeletedWhitelist(name) { - continue - } - - // 检查启动时间 - createTime, _ := p.CreateTime() - uptime := time.Now().UnixMilli() - createTime - if uptime < int64(pc.config.ProcessConfig.RecentStartupHours)*60*60*1000 { - suspiciousCount++ - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "deleted_binary", - Status: StatusWarn, - Message: fmt.Sprintf("可疑的已删除进程: %s (PID: %d, 路径: %s) - 最近%dh内启动", name, p.Pid, exe, pc.config.ProcessConfig.RecentStartupHours), - Evidence: pc.evidence.CollectProcessEvidence(p, "medium"), - }) - } - } - - // 5. 内存执行检测 (memfd) - if strings.Contains(exe, "memfd:") { - // 排除 runc / kubelet 等容器组件 - if !strings.Contains(cmdline, "runc") && !strings.Contains(name, "containerd") { - suspiciousCount++ - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "fileless_memfd", - Status: StatusFail, - Message: fmt.Sprintf("检测到无文件内存进程: %s (PID: %d)", name, p.Pid), - Evidence: pc.evidence.CollectProcessEvidence(p, "high"), - }) - } - } - } - - if suspiciousCount == 0 { - check.Message = "进程行为正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑进程", suspiciousCount) - } - - return check -} - -// CheckSuspiciousEnvVars 检查进程的可疑环境变量 -func (pc *ProcessChecker) CheckSuspiciousEnvVars() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "suspicious_env_vars", - Status: StatusPass, - Message: "进程环境变量检查", - Details: []protocol.SecurityCheckSub{}, - } - - procs, err := pc.cache.Get() - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取进程列表" - return check - } - - suspiciousCount := 0 - for _, p := range procs { - env, err := p.Environ() - if err != nil { - continue - } - - name, _ := p.Name() - exe, _ := p.Exe() - - for _, e := range env { - if strings.HasPrefix(e, "LD_PRELOAD=") || strings.HasPrefix(e, "LD_LIBRARY_PATH=") { - parts := strings.SplitN(e, "=", 2) - if len(parts) != 2 { - continue - } - value := parts[1] - - // 改用风险特征检测,只在真正可疑时报警 - riskLevel, reason := pc.assessLibraryRiskWithReason(value, name, exe) - - // 只报告中高风险的情况 - if riskLevel == "low" || riskLevel == "none" { - continue - } - - status := StatusWarn - if riskLevel == "high" { - status = StatusFail - check.Status = StatusFail - } else if check.Status != StatusFail { - check.Status = StatusWarn - } - - suspiciousCount++ - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_ld_env", - Status: status, - Message: fmt.Sprintf("进程 '%s'(PID: %d) 发现可疑环境变量: %s\n原因: %s", name, p.Pid, e, reason), - Evidence: pc.evidence.CollectProcessEvidence(p, riskLevel), - }) - break - } - } - } - - if suspiciousCount == 0 { - check.Message = "未发现可疑的进程环境变量" - } else { - check.Message = fmt.Sprintf("发现 %d 个进程存在可疑环境变量", suspiciousCount) - } - - return check -} - -// isMinerProcess 检查是否是挖矿进程 -func (pc *ProcessChecker) isMinerProcess(name, cmdline string) bool { - checkStr := strings.ToLower(name + " " + cmdline) - - // 检查关键词 - for _, keyword := range pc.config.ProcessConfig.MinerKeywords { - if strings.Contains(checkStr, keyword) { - return true - } - } - - // 检查挖矿命令特征 - if strings.Contains(cmdline, "--donate-level") || - strings.Contains(cmdline, "-o stratum+tcp") || - (strings.Contains(cmdline, "--pool") && strings.Contains(cmdline, "--user")) { - return true - } - - return false -} - -// isHighCPUMinerSuspect 检查是否是高 CPU 挖矿嫌疑进程 -func (pc *ProcessChecker) isHighCPUMinerSuspect(p *process.Process, name, exe string) bool { - cpuPercent, err := p.CPUPercent() - if err != nil || cpuPercent < pc.config.ProcessConfig.HighCPUThreshold { - return false - } - - // 检查白名单 - nameLower := strings.ToLower(name) - exeLower := strings.ToLower(exe) - - for _, whitelisted := range pc.config.ProcessConfig.HighCPUWhitelist { - if strings.Contains(nameLower, whitelisted) || strings.Contains(exeLower, whitelisted) { - return false - } - } - - // 检查是否有网络连接 - connections, err := p.Connections() - if err != nil || len(connections) == 0 { - return false - } - - // 检查是否有 ESTABLISHED 状态的外部连接 - for _, conn := range connections { - if conn.Status == "ESTABLISHED" && !IsLocalIP(conn.Raddr.IP) { - return true - } - } - - return false -} - -// isReverseShell 检查是否是反弹 shell -func (pc *ProcessChecker) isReverseShell(cmdline string) bool { - patterns := []string{ - "bash -i >& /dev/tcp", - "nc -e /bin/bash", - "nc -e /bin/sh", - "python -c 'import socket", - "perl -e 'use Socket", - "/bin/sh -i", - "/bin/bash -i", - } - - for _, pattern := range patterns { - if strings.Contains(cmdline, pattern) { - return true - } - } - - return false -} - -// isInDeletedWhitelist 检查是否在 deleted 进程白名单中 -func (pc *ProcessChecker) isInDeletedWhitelist(name string) bool { - for _, safeName := range pc.config.ProcessConfig.DeletedWhitelist { - if strings.Contains(strings.ToLower(name), safeName) { - return true - } - } - return false -} - -// isLegitimateLibraryPath 检查是否是合法的库路径 -func (pc *ProcessChecker) isLegitimateLibraryPath(path, processName, processExe string) bool { - pathLower := strings.ToLower(path) - processNameLower := strings.ToLower(processName) - processExeLower := strings.ToLower(processExe) - - // 现代应用打包系统 (Snap, Flatpak, AppImage) - // 这些系统会使用 LD_PRELOAD/LD_LIBRARY_PATH 加载隔离环境中的库 - appPackagingSystems := []string{ - "snap", "flatpak", "appimage", - } - for _, sys := range appPackagingSystems { - // 检查路径是否来自这些打包系统 - if strings.Contains(pathLower, "/"+sys+"/") || - strings.Contains(pathLower, "."+sys+"/") { - return true - } - // 检查进程是否由这些系统启动 - if strings.Contains(processExeLower, "/"+sys+"/") || - strings.Contains(processNameLower, sys) { - return true - } - } - - // 开发工具(IDE) - devTools := []string{ - "jetbrains", "vscode", "idea", "pycharm", "goland", "webstorm", - "clion", "rider", "datagrip", "androidstudio", - } - for _, tool := range devTools { - if strings.Contains(processExeLower, tool) { - return true - } - } - - // 数据库 - databases := []string{"postgres", "mysql", "mariadb", "mongodb", "redis", "elasticsearch", "clickhouse"} - for _, db := range databases { - if strings.Contains(processNameLower, db) { - return true - } - } - - // 容器相关 - containers := []string{"docker", "containerd", "kubelet", "podman", "cri-o", "k3s", "kubernetes"} - for _, container := range containers { - if strings.Contains(processNameLower, container) { - return true - } - } - - // Web 服务器 - webServers := []string{"nginx", "apache", "httpd", "caddy", "traefik"} - for _, ws := range webServers { - if strings.Contains(processNameLower, ws) { - return true - } - } - - // 科学计算和虚拟化 - scientificAndVirt := []string{ - "matlab", "mathematica", "octave", "julia", // 科学计算 - "virtualbox", "qemu", "vmware", "kvm", "libvirt", // 虚拟化 - "datadog", "newrelic", "prometheus", "grafana", // 监控工具 - } - for _, tool := range scientificAndVirt { - if strings.Contains(processNameLower, tool) || - strings.Contains(processExeLower, tool) { - return true - } - } - - // 系统库路径 - systemLibPaths := []string{ - "/usr/lib", "/lib", "/usr/local/lib", - "/opt/", "/usr/share", - } - for _, sysPath := range systemLibPaths { - if strings.HasPrefix(path, sysPath) { - return true - } - } - - return false -} - -// assessLibraryRiskWithReason 基于风险特征评估库路径的风险等级,返回风险等级和原因 -func (pc *ProcessChecker) assessLibraryRiskWithReason(path, processName, processExe string) (string, string) { - pathLower := strings.ToLower(path) - processNameLower := strings.ToLower(processName) - processExeLower := strings.ToLower(processExe) - - // 空路径 - if strings.TrimSpace(path) == "" { - return "none", "" - } - - // 1. 首先排除明显安全的情况 - - // 系统标准库路径 - 非常安全 - systemLibPaths := []string{ - "/usr/lib/", "/lib/", "/usr/local/lib/", - "/lib64/", "/usr/lib64/", "/usr/lib32/", - } - for _, sysPath := range systemLibPaths { - if strings.HasPrefix(path, sysPath) { - return "none", "" - } - } - - // 已知的应用打包系统 - 安全 - appPackagingSystems := []string{ - "/snap/", "/var/lib/snapd/snap/", - "/app/", "/var/lib/flatpak/", - "/.local/share/flatpak/", - } - for _, sys := range appPackagingSystems { - if strings.Contains(path, sys) { - return "none", "" - } - } - - // 常见的软件安装目录 - 相对安全 - commonAppPaths := []string{ - "/opt/", "/usr/share/", "/usr/local/share/", - "/Applications/", // macOS - } - for _, appPath := range commonAppPaths { - if strings.HasPrefix(path, appPath) { - return "none", "" - } - } - - // 2. 检查高风险特征 - - // 临时目录中的库 - 高风险 - highRiskPaths := []string{"/tmp/", "/dev/shm/", "/var/tmp/"} - for _, hrp := range highRiskPaths { - if strings.Contains(pathLower, hrp) { - return "high", fmt.Sprintf("库文件位于临时目录 %s,可能是恶意注入", hrp) - } - } - - // 隐藏目录/文件 + 非标准路径 - 中风险 - if strings.Contains(path, "/.") { - // 但如果是用户主目录下的隐藏配置目录,降低风险 - userConfigPaths := []string{ - "/.config/", "/.local/", "/.cache/", - "/.wine/", "/.steam/", "/.mozilla/", - } - isUserConfig := false - for _, userPath := range userConfigPaths { - if strings.Contains(pathLower, userPath) { - isUserConfig = true - break - } - } - if !isUserConfig { - return "medium", "库文件位于隐藏目录,且不是常见的用户配置目录" - } - } - - // 3. 检查进程本身是否可疑 - - // 如果进程路径也在临时目录,风险升级 - if processExe != "" { - for _, hrp := range highRiskPaths { - if strings.Contains(strings.ToLower(processExe), hrp) { - return "high", fmt.Sprintf("进程和库都位于临时目录,高度可疑") - } - } - } - - // 4. 检查进程名是否可疑 - suspiciousNames := []string{ - "xmrig", "miner", "kthreaddk", "kthreaddi", ".sh", - "python -c", "perl -e", "bash -i", "/dev/tcp", - } - for _, susName := range suspiciousNames { - if strings.Contains(processNameLower, susName) || strings.Contains(processExeLower, susName) { - return "high", fmt.Sprintf("进程名称或路径包含可疑特征: %s", susName) - } - } - - // 5. 其他情况 - 低风险或无风险 - - // 用户主目录下的应用 - 低风险(很多用户自己编译或安装的软件) - userHomePaths := []string{"/home/", "/root/", "/Users/"} - for _, homePath := range userHomePaths { - if strings.HasPrefix(path, homePath) { - // 但如果在主目录的明显的程序目录,就是安全的 - safeUserPaths := []string{ - "/bin/", "/apps/", "/software/", "/opt/", - "/go/", "/java/", "/python/", "/.jenv/", "/.pyenv/", "/.rbenv/", - } - for _, safePath := range safeUserPaths { - if strings.Contains(pathLower, safePath) { - return "none", "" - } - } - // 其他情况,低风险,不报警 - return "low", "" - } - } - - // 相对路径或当前目录 - 中等风险 - if strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") || !strings.HasPrefix(path, "/") { - return "medium", "使用相对路径加载库,可能是本地开发或潜在风险" - } - - // 默认:不在标准路径但也没有明显风险特征 - 低风险,不报警 - return "low", "" -} diff --git a/pkg/agent/audit/check_rootkit.go b/pkg/agent/audit/check_rootkit.go deleted file mode 100644 index 064aed4..0000000 --- a/pkg/agent/audit/check_rootkit.go +++ /dev/null @@ -1,207 +0,0 @@ -package audit - -import ( - "fmt" - "os" - "strconv" - "strings" - "time" - - "github.com/dushixiang/pika/internal/protocol" -) - -// RootkitChecker rootkit 检测器 -type RootkitChecker struct { - config *Config - cache *ProcessCache - evidence *EvidenceCollector -} - -// NewRootkitChecker 创建 rootkit 检测器 -func NewRootkitChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector) *RootkitChecker { - return &RootkitChecker{ - config: config, - cache: cache, - evidence: evidence, - } -} - -// Check 检测 rootkit 和恶意内核模块 -func (rc *RootkitChecker) Check() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "rootkit_detection", - Status: StatusPass, - Message: "Rootkit检测", - Details: []protocol.SecurityCheckSub{}, - } - - // 检查加载的内核模块 - suspiciousModules := rc.checkKernelModules() - if len(suspiciousModules) > 0 { - check.Status = StatusWarn - for _, mod := range suspiciousModules { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_module", - Status: StatusWarn, - Message: fmt.Sprintf("可疑内核模块: %s", mod), - }) - } - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "kernel_modules", - Status: StatusPass, - Message: "未发现可疑内核模块", - }) - } - - // 检查隐藏进程 - hiddenProcs := rc.checkHiddenProcesses() - if len(hiddenProcs) > 0 { - check.Status = StatusFail - for _, pid := range hiddenProcs { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "hidden_process", - Status: StatusFail, - Message: fmt.Sprintf("检测到隐藏进程: PID %d", pid), - }) - } - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "hidden_processes", - Status: StatusPass, - Message: "未发现隐藏进程", - }) - } - - // 更新总体消息 - switch check.Status { - case StatusPass: - check.Message = "未检测到rootkit" - case StatusWarn: - check.Message = "发现可疑rootkit特征" - case StatusFail: - check.Message = "发现rootkit感染迹象" - } - - return check -} - -// checkKernelModules 检查可疑内核模块 -func (rc *RootkitChecker) checkKernelModules() []string { - var suspicious []string - - content, err := os.ReadFile("/proc/modules") - if err != nil { - globalLogger.Warn("无法读取 /proc/modules: %v", err) - return suspicious - } - - // 已知的恶意模块名称特征 - maliciousPatterns := []string{ - "rootkit", "backdoor", "hidden", "stealth", - "diamorphine", "suterusu", "reptile", - } - - lines := strings.Split(string(content), "\n") - for _, line := range lines { - fields := strings.Fields(line) - if len(fields) > 0 { - modName := fields[0] - for _, pattern := range maliciousPatterns { - if strings.Contains(strings.ToLower(modName), pattern) { - suspicious = append(suspicious, modName) - globalLogger.Warn("发现可疑内核模块: %s", modName) - break - } - } - } - } - - return suspicious -} - -// checkHiddenProcesses 检查隐藏进程(优化版:二次确认机制) -func (rc *RootkitChecker) checkHiddenProcesses() []int32 { - // 第一轮快照 - hiddenCandidates := rc.getHiddenCandidates() - - if len(hiddenCandidates) == 0 { - return []int32{} - } - - globalLogger.Debug("发现 %d 个隐藏进程候选,开始二次确认", len(hiddenCandidates)) - - // 等待 100ms,消除进程刚好退出的时间差 - time.Sleep(100 * time.Millisecond) - - // 第二轮确认 - finalHidden := []int32{} - - // 重新获取当前的 gopsutil 列表 - procs, err := rc.cache.Get() - if err != nil { - globalLogger.Warn("二次确认时无法获取进程列表: %v", err) - return []int32{} - } - - currentApiPids := make(map[int32]bool) - for _, p := range procs { - currentApiPids[p.Pid] = true - } - - for _, pid := range hiddenCandidates { - // 检查 /proc/pid 是否依然存在 - if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil { - // /proc 下存在,但 API 列表里依然没有 - if !currentApiPids[pid] { - finalHidden = append(finalHidden, pid) - globalLogger.Warn("确认隐藏进程: PID %d", pid) - } - } - } - - return finalHidden -} - -// getHiddenCandidates 获取初步的隐藏进程候选 -func (rc *RootkitChecker) getHiddenCandidates() []int32 { - var hidden []int32 - - // 1. 扫描 /proc - procPids := make(map[int32]bool) - entries, err := os.ReadDir("/proc") - if err != nil { - globalLogger.Warn("无法读取 /proc: %v", err) - return hidden - } - - for _, entry := range entries { - if entry.IsDir() { - pid, err := strconv.Atoi(entry.Name()) - if err == nil { - procPids[int32(pid)] = true - } - } - } - - // 2. 扫描 API (gopsutil) - apiPids := make(map[int32]bool) - procs, err := rc.cache.Get() - if err != nil { - globalLogger.Warn("无法获取进程列表: %v", err) - return hidden - } - - for _, p := range procs { - apiPids[p.Pid] = true - } - - // 3. 对比 - for pid := range procPids { - if !apiPids[pid] { - hidden = append(hidden, pid) - } - } - - return hidden -} diff --git a/pkg/agent/audit/check_ssh.go b/pkg/agent/audit/check_ssh.go deleted file mode 100644 index 071e997..0000000 --- a/pkg/agent/audit/check_ssh.go +++ /dev/null @@ -1,387 +0,0 @@ -package audit - -import ( - "bufio" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "time" - - "github.com/dushixiang/pika/internal/protocol" -) - -// SSHChecker SSH 安全检查器 -type SSHChecker struct { - config *Config - cache *ProcessCache - evidence *EvidenceCollector - executor *CommandExecutor - strUtil *StringUtils -} - -// NewSSHChecker 创建 SSH 检查器 -func NewSSHChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector, executor *CommandExecutor) *SSHChecker { - return &SSHChecker{ - config: config, - cache: cache, - evidence: evidence, - executor: executor, - strUtil: &StringUtils{}, - } -} - -// Check 检查 SSH 后门和安全配置 -func (sc *SSHChecker) Check() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "ssh_security", - Status: StatusPass, - Message: "SSH安全检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 检查 SSH 服务是否运行 - if !sc.isSSHRunning() { - check.Message = "SSH服务未运行" - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ssh_status", - Status: StatusPass, - Message: "SSH未运行", - }) - return check - } - - // 1. 检查 authorized_keys 文件内容 - suspiciousKeys := sc.checkAuthorizedKeys() - if len(suspiciousKeys) > 0 { - check.Status = StatusWarn - for _, key := range suspiciousKeys { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_authorized_key", - Status: StatusWarn, - Message: key, - }) - } - } - - // 2. 检查 SSH 配置 - sshConfig := sc.readSSHConfig() - - // 检查是否允许 root 登录(仅记录,不告警) - //permitRoot := sshConfig["permitrootlogin"] - //if permitRoot != "" { - // check.Details = append(check.Details, protocol.SecurityCheckSub{ - // Name: "permit_root_login", - // Status: StatusPass, - // Message: fmt.Sprintf("Root登录策略: %s", permitRoot), - // }) - //} - - // 检查空密码登录 - permitEmpty := sshConfig["permitemptypasswords"] - if permitEmpty == "yes" { - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "permit_empty_passwords", - Status: StatusFail, - Message: "允许空密码登录", - }) - } - - // 检查密码认证 - passwordAuth := sshConfig["passwordauthentication"] - if passwordAuth == "yes" { - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "password_auth_enabled", - Status: StatusWarn, - Message: "启用了密码认证(建议仅使用密钥认证)", - }) - } - - // 3. 检查 SSH 二进制文件完整性 - for _, binPath := range sc.config.SSHConfig.BinaryPaths { - info, err := os.Stat(binPath) - if err != nil { - continue - } - - modTime := info.ModTime() - age := time.Since(modTime) - if age < time.Duration(sc.config.SSHConfig.RecentModifyDays)*24*time.Hour { - fileHash := sc.evidence.hashCache.GetSHA256(binPath) - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ssh_binary_modified", - Status: StatusWarn, - Message: fmt.Sprintf("SSH二进制文件最近被修改: %s (%d天前)", binPath, int(age.Hours()/24)), - Evidence: &protocol.Evidence{ - FilePath: binPath, - FileHash: fileHash, - Timestamp: modTime.UnixMilli(), - RiskLevel: "high", - }, - }) - } - } - - // 5. 检查 SSH 配置文件的软链接 - for _, configPath := range sc.config.SSHConfig.ConfigPaths { - linkTarget, err := os.Readlink(configPath) - if err == nil { - // 是软链接,检查目标是否可疑 - if strings.Contains(linkTarget, "/tmp") || strings.Contains(linkTarget, "/dev/shm") { - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ssh_config_symlink", - Status: StatusFail, - Message: fmt.Sprintf("SSH配置文件是指向可疑位置的软链接: %s -> %s", configPath, linkTarget), - }) - } - } - } - - // 更新总体消息 - switch check.Status { - case StatusPass: - check.Message = "SSH配置安全" - case StatusWarn: - check.Message = "SSH配置存在风险" - case StatusFail: - check.Message = "SSH配置存在严重风险" - } - - return check -} - -// isSSHRunning 检查 SSH 服务是否运行 -func (sc *SSHChecker) isSSHRunning() bool { - procs, err := sc.cache.Get() - if err != nil { - return false - } - - for _, p := range procs { - name, err := p.Name() - if err == nil && strings.Contains(name, "sshd") { - return true - } - } - - return false -} - -// checkAuthorizedKeys 检查 authorized_keys 文件内容 -func (sc *SSHChecker) checkAuthorizedKeys() []string { - var suspicious []string - - // 获取所有用户目录 - userDirs := sc.getAllUserDirectories() - - for _, userDir := range userDirs { - keyPath := filepath.Join(userDir.Path, ".ssh", "authorized_keys") - - info, err := os.Stat(keyPath) - if err != nil { - continue - } - - // 检查文件大小 - if info.Size() > sc.config.SSHConfig.MaxAuthorizedKeysSize { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 文件过大 (%.2f MB),可能是异常", - userDir.Username, float64(info.Size())/1024/1024)) - } - - // 检查最近修改 - if time.Since(info.ModTime()) < time.Duration(sc.config.SSHConfig.AuthKeysRecentModifyDays)*24*time.Hour { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 最近被修改: %s (%d天前)", - userDir.Username, info.ModTime().Format("2006-01-02 15:04:05"), int(time.Since(info.ModTime()).Hours()/24))) - } - - // 检查文件内容 - f, err := os.Open(keyPath) - if err != nil { - continue - } - - reader := io.LimitReader(f, sc.config.PerformanceConfig.AuthKeysReadLimitKB*1024) - content, err := io.ReadAll(reader) - f.Close() - if err != nil { - continue - } - - lines := strings.Split(string(content), "\n") - keyCount := 0 - - for lineNum, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - // 检查可疑的公钥选项 - if strings.Contains(line, "command=") { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥包含command选项(行%d): %s...", - userDir.Username, lineNum+1, sc.strUtil.Truncate(line, 60))) - } - - if strings.Contains(line, "from=") { - if strings.Contains(line, `from="*"`) || strings.Contains(line, "from=*") { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥允许任意来源(from=*): 行%d", userDir.Username, lineNum+1)) - } - } - - // 统计公钥数量 - if strings.HasPrefix(line, "ssh-rsa") || strings.HasPrefix(line, "ssh-ed25519") || strings.HasPrefix(line, "ecdsa-") { - keyCount++ - } - } - - if keyCount > sc.config.SSHConfig.MaxKeysCount { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 拥有大量公钥(%d个), 建议审查", userDir.Username, keyCount)) - } - - // 检查文件权限 - if info.Mode().Perm()&0022 != 0 { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 权限过高: %o (Group/Other不可写)", userDir.Username, info.Mode().Perm())) - } - } - - return suspicious -} - -// readSSHConfig 读取 SSH 配置 -func (sc *SSHChecker) readSSHConfig() map[string]string { - config := make(map[string]string) - - // 优先使用 sshd -T - output, err := sc.executor.Execute("sshd", "-T") - if err == nil { - lines := strings.Split(output, "\n") - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) >= 2 { - config[strings.ToLower(parts[0])] = parts[1] - } - } - return config - } - - // 备用:读配置文件 - for _, path := range sc.config.SSHConfig.ConfigPaths { - content, err := os.ReadFile(path) - if err != nil { - continue - } - - lines := strings.Split(string(content), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - parts := strings.Fields(line) - if len(parts) >= 2 { - config[strings.ToLower(parts[0])] = parts[1] - } - } - break - } - - return config -} - -// UserDirectory 用户目录信息 -type UserDirectory struct { - Username string - Path string - Source string -} - -// getAllUserDirectories 获取所有用户目录 -func (sc *SSHChecker) getAllUserDirectories() []UserDirectory { - var userDirs []UserDirectory - seenPaths := make(map[string]bool) - - // 1. 从 /etc/passwd 获取用户 - users := sc.getAllUsers() - for _, user := range users { - if user.Home != "" && !seenPaths[user.Home] { - userDirs = append(userDirs, UserDirectory{ - Username: user.Username, - Path: user.Home, - Source: "passwd", - }) - seenPaths[user.Home] = true - } - } - - // 2. 扫描 /home 目录,发现 LDAP/AD 等外部认证用户 - entries, err := os.ReadDir("/home") - if err != nil { - return userDirs - } - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - homePath := filepath.Join("/home", entry.Name()) - - if seenPaths[homePath] { - continue - } - - // 检查是否有 .ssh 目录 - sshPath := filepath.Join(homePath, ".ssh") - if stat, err := os.Stat(sshPath); err == nil && stat.IsDir() { - userDirs = append(userDirs, UserDirectory{ - Username: entry.Name() + " (LDAP/AD)", - Path: homePath, - Source: "home_scan", - }) - seenPaths[homePath] = true - } - } - - return userDirs -} - -// UserInfo 用户信息 -type UserInfo struct { - Username string - UID string - Home string - Shell string -} - -// getAllUsers 获取所有用户 -func (sc *SSHChecker) getAllUsers() []UserInfo { - var users []UserInfo - - file, err := os.Open("/etc/passwd") - if err != nil { - return users - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, ":") - if len(parts) >= 7 { - users = append(users, UserInfo{ - Username: parts[0], - UID: parts[2], - Home: parts[5], - Shell: parts[6], - }) - } - } - - return users -} diff --git a/web/assets.go b/web/assets.go index 8b0c87e..028a0ff 100644 --- a/web/assets.go +++ b/web/assets.go @@ -2,6 +2,7 @@ package web import ( "embed" + "encoding/base64" "io/fs" ) @@ -19,3 +20,20 @@ var indexHtml string func IndexHtml() string { return indexHtml } + +//go:embed public/logo.png +var defaultLogo []byte + +func DefaultLogo() []byte { + return defaultLogo +} + +// DefaultLogoBase64 返回默认 Logo 的 base64 编码(data URI 格式) +func DefaultLogoBase64() string { + if len(defaultLogo) == 0 { + return "" + } + // 编码为 base64 并添加 data URI 前缀 + encoded := base64.StdEncoding.EncodeToString(defaultLogo) + return "data:image/png;base64," + encoded +} diff --git a/web/index.html b/web/index.html index 91c30f2..7515cd1 100644 --- a/web/index.html +++ b/web/index.html @@ -9,6 +9,7 @@ window.SystemConfig = { SystemNameZh: "{{.SystemNameZh}}", SystemNameEn: "{{.SystemNameEn}}", + ICPCode: "{{.ICPCode}}", }; diff --git a/web/src/api/agent.ts b/web/src/api/agent.ts index ce8f137..e63748c 100644 --- a/web/src/api/agent.ts +++ b/web/src/api/agent.ts @@ -111,20 +111,12 @@ export interface SystemInfo { publicIP?: string; } -export interface Evidence { - fileHash?: string; - processTree?: string[]; - filePath?: string; - timestamp?: number; - networkConn?: string; - riskLevel?: 'low' | 'medium' | 'high'; -} - export interface SecurityCheckSub { - name: string; + name?: string; status: 'pass' | 'fail' | 'warn' | 'skip'; + severity?: 'high' | 'medium' | 'low'; message: string; - evidence?: Evidence; + evidence?: string; // 简化为字符串 } export interface SecurityCheck { @@ -134,14 +126,104 @@ export interface SecurityCheck { details?: SecurityCheckSub[]; } +// 资产清单相关类型 +export interface ListeningPort { + protocol: string; + address: string; + port: number; + processName?: string; + processPid?: number; + processPath?: string; + isPublic: boolean; +} + +export interface NetworkAssets { + listeningPorts?: ListeningPort[]; + connections?: any[]; + interfaces?: any[]; +} + +export interface ProcessInfo { + pid: number; + name: string; + cmdline?: string; + exe?: string; + username?: string; + cpuPercent: number; + memPercent: number; + memoryMb: number; +} + +export interface ProcessAssets { + topCpuProcesses?: ProcessInfo[]; + topMemoryProcesses?: ProcessInfo[]; +} + +export interface UserInfo { + username: string; + uid: string; + gid: string; + homeDir?: string; + shell?: string; + isLoginable: boolean; + isRootEquiv: boolean; + hasPassword: boolean; +} + +export interface UserAssets { + systemUsers?: UserInfo[]; + loginHistory?: any[]; + sshKeys?: any[]; + sudoUsers?: any[]; +} + +export interface FileAssets { + cronJobs?: any[]; + systemdServices?: any[]; + startupScripts?: any[]; + recentModified?: any[]; + largeFiles?: any[]; +} + +export interface KernelAssets { + loadedModules?: any[]; + kernelParameters?: Record; + securityModules?: any; +} + +export interface AssetInventory { + networkAssets?: NetworkAssets; + processAssets?: ProcessAssets; + userAssets?: UserAssets; + fileAssets?: FileAssets; + kernelAssets?: KernelAssets; +} + +export interface AuditStatistics { + networkStats?: any; + processStats?: any; + userStats?: any; + fileStats?: any; +} + +// VPS审计结果(Agent端收集的原始数据) export interface VPSAuditResult { systemInfo: SystemInfo; - securityChecks: SecurityCheck[]; + assetInventory: AssetInventory; + statistics: AuditStatistics; startTime: number; endTime: number; + collectWarnings?: string[]; +} + +// VPS安全分析结果(Server端分析后的结果) +export interface VPSAuditAnalysis { + auditId: string; + securityChecks: SecurityCheck[]; riskScore: number; threatLevel: 'low' | 'medium' | 'high' | 'critical'; recommendations?: string[]; + analyzedAt: number; } export interface AuditResultSummary { diff --git a/web/src/api/system-config.ts b/web/src/api/system-config.ts index bd7f943..48260fa 100644 --- a/web/src/api/system-config.ts +++ b/web/src/api/system-config.ts @@ -6,26 +6,15 @@ export interface SystemConfig { systemNameEn: string; // 英文名称 systemNameZh: string; // 中文名称 logoBase64: string; // Logo 的 base64 编码 + icpCode: string; // ICP 备案号 } -// 默认系统配置 -export const DEFAULT_SYSTEM_CONFIG: SystemConfig = { - systemNameEn: 'Pika Monitor', - systemNameZh: '皮卡监控', - logoBase64: '', // 默认为空,将使用 /logo.png -}; - // 获取系统配置(管理后台使用) export const getSystemConfig = async (): Promise => { - try { - const response = await get<{ id: string; name: string; value: SystemConfig }>( - `/admin/properties/${PROPERTY_ID_SYSTEM_CONFIG}` - ); - return response.data.value || DEFAULT_SYSTEM_CONFIG; - } catch (error) { - // 如果配置不存在,返回默认值 - return DEFAULT_SYSTEM_CONFIG; - } + const response = await get<{ id: string; name: string; value: SystemConfig }>( + `/admin/properties/${PROPERTY_ID_SYSTEM_CONFIG}` + ); + return response.data.value; }; // 保存系统配置 diff --git a/web/src/components/PublicFooter.tsx b/web/src/components/PublicFooter.tsx index b52e14d..f57932b 100644 --- a/web/src/components/PublicFooter.tsx +++ b/web/src/components/PublicFooter.tsx @@ -2,6 +2,7 @@ import {Heart} from 'lucide-react'; const PublicFooter = () => { const currentYear = new Date().getFullYear(); + const icpCode = window.SystemConfig?.ICPCode || ''; return (