优化代码

This commit is contained in:
dushixiang committed 2025-11-26 21:49:15 +08:00
1 parent b446600dfe
commit ab24de67f5
31 files changed
+2695 -2429

No files matched your search

+2 -19
View File
@@ -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 {
+1 -1
View File
@@ -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()
+1
View File
@@ -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备案号
}
+315 -11
View File
@@ -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"` // 安全启动状态
}
+13
View File
@@ -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
-41
View File
@@ -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
}
+23 -28
View File
@@ -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,
})
}
+67 -5
View File
@@ -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
}
+340
View File
@@ -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
}
+19
View File
@@ -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)
}
}
+16
View File
@@ -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
}
+153
View File
@@ -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
}
+433
View File
@@ -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"
}
+155
View File
@@ -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
}
+392
View File
@@ -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
}
+89 -115
View File
@@ -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)
}
-419
View File
@@ -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
}
-270
View File
@@ -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
}
-513
View File
@@ -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", ""
}
-207
View File
@@ -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
}
-387
View File
@@ -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
}
+18
View File
@@ -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
}
+1
View File
@@ -9,6 +9,7 @@
window.SystemConfig = {
SystemNameZh: "{{.SystemNameZh}}",
SystemNameEn: "{{.SystemNameEn}}",
ICPCode: "{{.ICPCode}}",
};
</script>
</head>
+94 -12
View File
@@ -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<string, string>;
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 {
+5 -16
View File
@@ -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<SystemConfig> => {
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;
};
// 保存系统配置
+15
View File
@@ -2,6 +2,7 @@ import {Heart} from 'lucide-react';
const PublicFooter = () => {
const currentYear = new Date().getFullYear();
const icpCode = window.SystemConfig?.ICPCode || '';
return (
<footer className="border-t border-slate-100 bg-gradient-to-b from-white to-slate-50">
@@ -24,6 +25,20 @@ const PublicFooter = () => {
</a>
<span className="text-slate-300">·</span>
<span>保持洞察,稳定运行</span>
{/* ICP 备案号 */}
{icpCode && (
<>
<span className="text-slate-300">·</span>
<a
href="https://beian.miit.gov.cn"
target="_blank"
rel="noopener noreferrer"
className="text-slate-500 hover:text-slate-700 transition-colors"
>
{icpCode}
</a>
</>
)}
</div>
<div className="flex items-center gap-1">
<span>用</span>
+1
View File
@@ -5,6 +5,7 @@ declare global {
SystemConfig: {
SystemNameZh: string;
SystemNameEn: string;
ICPCode: string;
};
}
}
+10 -95
View File
@@ -16,7 +16,12 @@ import {
FileWarning
} from 'lucide-react';
import TamperProtection from './TamperProtection.tsx';
import {getAgentForAdmin, getAuditResult, sendAuditCommand, type VPSAuditResult} from '../../api/agent';
import {
getAgentForAdmin,
getAuditResult,
sendAuditCommand,
type VPSAuditResult
} from '../../api/agent';
import type {Agent} from '../../types';
import dayjs from 'dayjs';
import {getErrorMessage} from '../../lib/utils';
@@ -66,10 +71,10 @@ const AgentDetail = () => {
await sendAuditCommand(id);
messageApi.success('安全审计已启动,请稍后查看结果');
// 5秒后刷新结果
// 10秒后刷新结果 (给Server端分析时间)
setTimeout(() => {
fetchData();
}, 5000);
}, 10000);
} catch (error: unknown) {
messageApi.error(getErrorMessage(error, '启动审计失败'));
} finally {
@@ -113,11 +118,6 @@ const AgentDetail = () => {
},
];
// 计算审计统计
const passCount = auditResult?.securityChecks.filter(c => c.status === 'pass').length || 0;
const failCount = auditResult?.securityChecks.filter(c => c.status === 'fail').length || 0;
const warnCount = auditResult?.securityChecks.filter(c => c.status === 'warn').length || 0;
const totalCount = auditResult?.securityChecks.length || 0;
// Tab 项配置
const tabItems: TabsProps['items'] = [
@@ -185,13 +185,10 @@ const AgentDetail = () => {
<div className="flex items-center gap-2 text-sm">
<Shield size={16}/>
<div>安全审计</div>
{auditResult && failCount > 0 && (
<Tag color="error" style={{marginLeft: 4}}>{failCount}</Tag>
)}
</div>
),
children: (
<Space direction="vertical">
<Space direction="vertical" style={{width: '100%'}}>
{/* 非 Linux 系统提示 */}
{agent && !agent.os.toLowerCase().includes('linux') && (
<Alert
@@ -224,77 +221,7 @@ const AgentDetail = () => {
/>
) : null
) : (
<>
{/* 审计概览 */}
<Card bordered={false}>
<Row gutter={[16, 16]}>
<Col xs={12} sm={6}>
<Statistic
title="总检查项"
value={totalCount}
prefix={<Shield size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={6}>
<Statistic
title="通过"
value={passCount}
valueStyle={{color: '#52c41a'}}
prefix={<CheckCircle size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={6}>
<Statistic
title="失败"
value={failCount}
valueStyle={{color: '#ff4d4f'}}
prefix={<XCircle size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={6}>
<Statistic
title="警告"
value={warnCount}
valueStyle={{color: '#faad14'}}
prefix={<AlertTriangle size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={12}>
<Statistic
title="风险评分"
value={auditResult.riskScore}
suffix="/ 100"
valueStyle={{
color: auditResult.riskScore >= 80 ? '#ff4d4f' :
auditResult.riskScore >= 50 ? '#faad14' :
auditResult.riskScore >= 20 ? '#1890ff' : '#52c41a'
}}
prefix={<Activity size={18} className="lg:w-5 lg:h-5"/>}
/>
</Col>
<Col xs={12} sm={12}>
<div className="text-sm text-gray-500 mb-2">威胁等级</div>
<Tag
color={
auditResult.threatLevel === 'critical' || auditResult.threatLevel === 'high' ? 'error' :
auditResult.threatLevel === 'medium' ? 'warning' : 'success'
}
className="text-base px-3 py-1"
>
{auditResult.threatLevel === 'critical' ? '严重风险' :
auditResult.threatLevel === 'high' ? '高风险' :
auditResult.threatLevel === 'medium' ? '中风险' : '低风险'}
</Tag>
</Col>
</Row>
<div className="mt-4 text-sm text-gray-500">
最近审计时间: {dayjs(auditResult.startTime).format('YYYY-MM-DD HH:mm:ss')}
</div>
</Card>
{/* 详细审计结果 */}
<AuditResultView result={auditResult}/>
</>
<AuditResultView result={auditResult}/>
)}
</Space>
),
@@ -368,18 +295,6 @@ const AgentDetail = () => {
</Space>
</div>
</div>
{auditResult && (
<div className="text-center">
<div className="text-sm text-gray-500">安全评分</div>
<div className={`text-3xl font-bold ${
auditResult.riskScore >= 80 ? 'text-red-500' :
auditResult.riskScore >= 50 ? 'text-orange-500' :
auditResult.riskScore >= 20 ? 'text-blue-500' : 'text-green-500'
}`}>
{100 - auditResult.riskScore}
</div>
</div>
)}
</div>
</Card>
+386 -156
View File
@@ -1,15 +1,21 @@
import {Card, Collapse, List, Space, Tag, Descriptions} from 'antd';
import {Card, Collapse, List, Space, Tag, Descriptions, Tabs, Table, Empty, Alert} from 'antd';
import {
CheckCircle,
XCircle,
AlertTriangle,
MinusCircle,
Server,
Network,
Cpu,
Users,
FileText,
Hash,
GitBranch,
Clock
Shield,
Activity,
Calendar,
Settings,
PlayCircle
} from 'lucide-react';
import type {VPSAuditResult} from '../../api/agent';
import type {VPSAuditResult, VPSAuditAnalysis} from '../../api/agent';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
@@ -48,28 +54,26 @@ const AuditResultView = ({result}: AuditResultViewProps) => {
return <Tag color={config.color}>{config.text}</Tag>;
};
const getCategoryName = (category: string) => {
const names: Record<string, string> = {
'non_root_user': '非Root用户',
'ufw_security': 'UFW防火墙',
'ssh_security': 'SSH安全',
'access_control': '访问控制',
'port_security': '端口安全',
'unattended_upgrades': '自动更新',
'fail2ban': 'Fail2ban',
'rootkit_detection': 'Rootkit检测',
'suspicious_processes': '可疑进程检测',
'listening_ports': '端口监听检查',
'cron_jobs': '定时任务检查',
'suspicious_files': '可疑文件检查',
'system_accounts': '系统账户检查',
'network_connections': '网络连接检查',
'file_integrity': '文件完整性检查',
'login_history': '登录历史检查',
'immutable_files': '不可变文件检查',
'suspicious_env_vars': '环境变量检查',
const getSeverityTag = (severity?: string) => {
if (!severity) return null;
const configs = {
high: {color: 'error', text: '高危'},
medium: {color: 'warning', text: '中危'},
low: {color: 'default', text: '低危'},
};
return names[category] || category;
const config = configs[severity as keyof typeof configs];
return config ? <Tag color={config.color}>{config.text}</Tag> : null;
};
const getThreatLevelTag = (level: string) => {
const configs = {
critical: {color: 'error', text: '危急'},
high: {color: 'error', text: '高危'},
medium: {color: 'warning', text: '中危'},
low: {color: 'success', text: '低危'},
};
const config = configs[level as keyof typeof configs] || {color: 'default', text: level};
return <Tag color={config.color} className="text-lg px-3 py-1">{config.text}</Tag>;
};
const formatUptime = (seconds: number) => {
@@ -80,11 +84,135 @@ const AuditResultView = ({result}: AuditResultViewProps) => {
return `${days}天 ${hours}小时 ${minutes}分钟`;
};
const portColumns = [
{title: '协议', dataIndex: 'protocol', key: 'protocol', width: 80},
{
title: '地址:端口',
key: 'address',
render: (record: any) => `${record.address}:${record.port}`,
},
{
title: '公网暴露',
dataIndex: 'isPublic',
key: 'isPublic',
width: 100,
render: (isPublic: boolean) => isPublic ?
<Tag color="warning">是</Tag> : <Tag color="default">否</Tag>
},
{title: '进程', dataIndex: 'processName', key: 'processName'},
{title: 'PID', dataIndex: 'processPid', key: 'processPid', width: 80},
];
const connectionColumns = [
{title: '协议', dataIndex: 'protocol', key: 'protocol', width: 80},
{
title: '本地地址',
key: 'local',
render: (record: any) => `${record.laddr?.ip || ''}:${record.laddr?.port || ''}`,
},
{
title: '远程地址',
key: 'remote',
render: (record: any) => `${record.raddr?.ip || ''}:${record.raddr?.port || ''}`,
},
{title: '状态', dataIndex: 'status', key: 'status', width: 120},
{title: 'PID', dataIndex: 'pid', key: 'pid', width: 80},
{title: '进程', dataIndex: 'processName', key: 'processName'},
];
const processColumns = [
{title: 'PID', dataIndex: 'pid', key: 'pid', width: 80},
{title: '进程名', dataIndex: 'name', key: 'name'},
{title: '用户', dataIndex: 'username', key: 'username', width: 100},
{
title: 'CPU %',
dataIndex: 'cpuPercent',
key: 'cpuPercent',
width: 100,
render: (val: number) => `${val.toFixed(2)}%`
},
{
title: '内存 %',
dataIndex: 'memPercent',
key: 'memPercent',
width: 100,
render: (val: number) => `${val.toFixed(2)}%`
},
{
title: '内存 (MB)',
dataIndex: 'memoryMb',
key: 'memoryMb',
width: 120,
render: (val: number) => `${val} MB`
},
];
const userColumns = [
{title: '用户名', dataIndex: 'username', key: 'username'},
{title: 'UID', dataIndex: 'uid', key: 'uid', width: 80},
{title: 'GID', dataIndex: 'gid', key: 'gid', width: 80},
{title: 'Shell', dataIndex: 'shell', key: 'shell'},
{
title: '可登录',
dataIndex: 'isLoginable',
key: 'isLoginable',
width: 100,
render: (val: boolean) => val ? <Tag color="success">是</Tag> : <Tag color="default">否</Tag>
},
{
title: 'Root权限',
dataIndex: 'isRootEquiv',
key: 'isRootEquiv',
width: 100,
render: (val: boolean) => val ? <Tag color="error">是</Tag> : <Tag color="default">否</Tag>
},
];
const cronColumns = [
{title: '用户', dataIndex: 'user', key: 'user', width: 100},
{title: '计划', dataIndex: 'schedule', key: 'schedule', width: 150},
{title: '命令', dataIndex: 'command', key: 'command', ellipsis: true},
{title: '文件路径', dataIndex: 'filePath', key: 'filePath', width: 200, ellipsis: true},
];
const serviceColumns = [
{title: '服务名', dataIndex: 'name', key: 'name'},
{title: '状态', dataIndex: 'state', key: 'state', width: 100},
{
title: '开机启动',
dataIndex: 'enabled',
key: 'enabled',
width: 100,
render: (val: boolean) => val ? <Tag color="success">是</Tag> : <Tag color="default">否</Tag>
},
{title: '启动命令', dataIndex: 'execStart', key: 'execStart', ellipsis: true},
{title: '描述', dataIndex: 'description', key: 'description', ellipsis: true},
];
const startupScriptColumns = [
{title: '类型', dataIndex: 'type', key: 'type', width: 150},
{title: '名称', dataIndex: 'name', key: 'name'},
{title: '路径', dataIndex: 'path', key: 'path', ellipsis: true},
{
title: '启用',
dataIndex: 'enabled',
key: 'enabled',
width: 80,
render: (val: boolean) => val ? <Tag color="success">是</Tag> : <Tag color="default">否</Tag>
},
];
const moduleColumns = [
{title: '模块名', dataIndex: 'name', key: 'name'},
{title: '大小', dataIndex: 'size', key: 'size', width: 120, render: (val: number) => `${val} bytes`},
{title: '被引用次数', dataIndex: 'usedBy', key: 'usedBy', width: 120},
];
return (
<div className="space-y-4">
<Space direction="vertical" size="large" style={{width: '100%'}}>
{/* 系统信息 */}
<Card
title={<span className="font-semibold">系统信息</span>}
title={<Space><Server size={18}/><span className="font-semibold">系统信息</span></Space>}
variant={'outlined'}
>
<Descriptions column={{xs: 1, sm: 2}} bordered>
@@ -105,7 +233,7 @@ const AuditResultView = ({result}: AuditResultViewProps) => {
{result.systemInfo.publicIP}
</Descriptions.Item>
)}
<Descriptions.Item label="审计时间" span={2}>
<Descriptions.Item label="采集时间" span={2}>
{dayjs(result.startTime).format('YYYY-MM-DD HH:mm:ss')} - {dayjs(result.endTime).format('HH:mm:ss')}
<span className="ml-2 text-gray-500">
(耗时: {((result.endTime - result.startTime) / 1000).toFixed(2)}秒)
@@ -114,140 +242,242 @@ const AuditResultView = ({result}: AuditResultViewProps) => {
</Descriptions>
</Card>
{/* 安全检查结果 */}
{/* 资产清单 */}
<Card
title={<span className="font-semibold">安全检查结果</span>}
title={<Space><Activity size={18}/><span className="font-semibold">资产清单</span></Space>}
variant={'outlined'}
>
<Collapse accordion>
{result.securityChecks.map((check, index) => (
<Panel
key={index}
header={
<Space>
{getStatusIcon(check.status)}
<span className="font-medium">
{getCategoryName(check.category)}
</span>
{getStatusTag(check.status)}
<span className="text-gray-500 text-sm">
{check.message}
</span>
</Space>
}
>
{check.details && check.details.length > 0 && (
<List
size="small"
dataSource={check.details}
renderItem={(detail) => (
<List.Item className="flex-col items-start">
<Space className="w-full">
{getStatusIcon(detail.status)}
<span className="font-mono text-sm">
{detail.name}
</span>
{getStatusTag(detail.status)}
<span className="text-gray-600 flex-1">
{detail.message}
</span>
</Space>
{detail.evidence && (
<div className="ml-6 mt-2 p-3 bg-gray-50 rounded text-xs w-full">
<div className="font-semibold mb-2 flex items-center gap-2">
<FileText size={14}/>
证据信息
{detail.evidence.riskLevel && (
<Tag
color={
detail.evidence.riskLevel === 'high' ? 'error' :
detail.evidence.riskLevel === 'medium' ? 'warning' : 'default'
}
className="ml-2"
>
{detail.evidence.riskLevel === 'high' ? '高危' :
detail.evidence.riskLevel === 'medium' ? '中危' : '低危'}
</Tag>
)}
</div>
{detail.evidence.filePath && (
<div className="mb-1">
<span className="font-medium">文件路径: </span>
<code className="bg-white px-1">{detail.evidence.filePath}</code>
</div>
)}
{detail.evidence.fileHash && (
<div className="mb-1 flex items-start gap-1">
<Hash size={12} className="mt-0.5"/>
<span className="font-medium">SHA256: </span>
<code className="bg-white px-1 break-all">{detail.evidence.fileHash}</code>
</div>
)}
{detail.evidence.timestamp && (
<div className="mb-1 flex items-center gap-1">
<Clock size={12}/>
<span className="font-medium">时间: </span>
{dayjs(detail.evidence.timestamp).format('YYYY-MM-DD HH:mm:ss')}
</div>
)}
{detail.evidence.networkConn && (
<div className="mb-1">
<span className="font-medium">网络连接: </span>
<code className="bg-white px-1">{detail.evidence.networkConn}</code>
</div>
)}
{detail.evidence.processTree && detail.evidence.processTree.length > 0 && (
<div className="mt-2">
<div className="font-medium mb-1 flex items-center gap-1">
<GitBranch size={12}/>
进程树:
</div>
<div className="bg-white p-2 rounded font-mono text-xs overflow-x-auto">
{detail.evidence.processTree.map((line, idx) => (
<div key={idx} className="whitespace-nowrap">
{line}
</div>
))}
</div>
</div>
)}
</div>
)}
</List.Item>
<Tabs
items={[
{
key: 'network',
label: <Space><Network size={16}/>网络资产</Space>,
children: (
<Space direction="vertical" size="middle" style={{width: '100%'}}>
<Card size="small" title="监听端口">
{result.assetInventory.networkAssets?.listeningPorts?.length ? (
<Table
size="small"
dataSource={result.assetInventory.networkAssets.listeningPorts}
columns={portColumns}
rowKey={(record, index) => `${record.address}:${record.port}-${index}`}
pagination={false}
/>
) : (
<Empty description="无数据"/>
)}
</Card>
<Card size="small" title="活跃连接">
{result.assetInventory.networkAssets?.connections?.length ? (
<Table
size="small"
dataSource={result.assetInventory.networkAssets.connections}
columns={connectionColumns}
rowKey={(record, index) => `${record.laddr?.ip}:${record.laddr?.port}-${record.raddr?.ip}:${record.raddr?.port}-${index}`}
pagination={false}
/>
) : (
<Empty description="无数据"/>
)}
</Card>
{result.statistics?.networkStats && (
<Card size="small" title="网络统计">
<Descriptions size="small" column={3}>
<Descriptions.Item label="监听端口总数">
{result.statistics.networkStats.totalListeningPorts || 0}
</Descriptions.Item>
<Descriptions.Item label="公网端口">
{result.statistics.networkStats.publicListeningPorts || 0}
</Descriptions.Item>
<Descriptions.Item label="活跃连接">
{result.statistics.networkStats.activeConnections || 0}
</Descriptions.Item>
</Descriptions>
</Card>
)}
/>
)}
</Panel>
))}
</Collapse>
</Space>
),
},
{
key: 'process',
label: <Space><Cpu size={16}/>进程资产</Space>,
children: (
<Space direction="vertical" size="middle" style={{width: '100%'}}>
<Card size="small" title="TOP CPU进程">
{result.assetInventory.processAssets?.topCpuProcesses?.length ? (
<Table
size="small"
dataSource={result.assetInventory.processAssets.topCpuProcesses}
columns={processColumns}
rowKey="pid"
pagination={false}
/>
) : (
<Empty description="无数据"/>
)}
</Card>
<Card size="small" title="TOP 内存进程">
{result.assetInventory.processAssets?.topMemoryProcesses?.length ? (
<Table
size="small"
dataSource={result.assetInventory.processAssets.topMemoryProcesses}
columns={processColumns}
rowKey="pid"
pagination={false}
/>
) : (
<Empty description="无数据"/>
)}
</Card>
</Space>
),
},
{
key: 'user',
label: <Space><Users size={16}/>用户资产</Space>,
children: (
<Card size="small" title="系统用户">
{result.assetInventory.userAssets?.systemUsers?.length ? (
<Table
size="small"
dataSource={result.assetInventory.userAssets.systemUsers}
columns={userColumns}
rowKey="username"
pagination={false}
/>
) : (
<Empty description="无数据"/>
)}
</Card>
),
},
{
key: 'file',
label: <Space><FileText size={16}/>文件资产</Space>,
children: (
<Space direction="vertical" size="middle" style={{width: '100%'}}>
<Card size="small" title={<Space><Calendar size={16}/>定时任务</Space>}>
{result.assetInventory.fileAssets?.cronJobs?.length ? (
<Table
size="small"
dataSource={result.assetInventory.fileAssets.cronJobs}
columns={cronColumns}
rowKey={(record, index) => `${record.user}-${index}`}
pagination={false}
/>
) : (
<Empty description="无定时任务"/>
)}
</Card>
<Card size="small" title={<Space><Settings size={16}/>Systemd服务</Space>}>
{result.assetInventory.fileAssets?.systemdServices?.length ? (
<Table
size="small"
dataSource={result.assetInventory.fileAssets.systemdServices}
columns={serviceColumns}
rowKey="name"
pagination={false}
/>
) : (
<Empty description="无Systemd服务"/>
)}
</Card>
<Card size="small" title={<Space><PlayCircle size={16}/>启动脚本</Space>}>
{result.assetInventory.fileAssets?.startupScripts?.length ? (
<Table
size="small"
dataSource={result.assetInventory.fileAssets.startupScripts}
columns={startupScriptColumns}
rowKey={(record, index) => `${record.path}-${index}`}
pagination={false}
/>
) : (
<Empty description="无启动脚本"/>
)}
</Card>
</Space>
),
},
{
key: 'kernel',
label: <Space><Shield size={16}/>内核信息</Space>,
children: (
<Space direction="vertical" size="middle" style={{width: '100%'}}>
<Card size="small" title="已加载内核模块">
{result.assetInventory.kernelAssets?.loadedModules?.length ? (
<Table
size="small"
dataSource={result.assetInventory.kernelAssets.loadedModules}
columns={moduleColumns}
rowKey="name"
pagination={false}
/>
) : (
<Empty description="无已加载模块"/>
)}
</Card>
{result.assetInventory.kernelAssets?.securityModules && (
<Card size="small" title="安全模块状态">
<Descriptions size="small" column={1}>
<Descriptions.Item label="SELinux">
<Tag color={
result.assetInventory.kernelAssets.securityModules.selinuxStatus === 'enforcing' ? 'success' :
result.assetInventory.kernelAssets.securityModules.selinuxStatus === 'permissive' ? 'warning' : 'default'
}>
{result.assetInventory.kernelAssets.securityModules.selinuxStatus || 'unknown'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="AppArmor">
<Tag color={
result.assetInventory.kernelAssets.securityModules.apparmorStatus === 'enabled' ? 'success' : 'default'
}>
{result.assetInventory.kernelAssets.securityModules.apparmorStatus || 'unknown'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Secure Boot">
<Tag color={
result.assetInventory.kernelAssets.securityModules.secureBootState === 'enabled' ? 'success' : 'default'
}>
{result.assetInventory.kernelAssets.securityModules.secureBootState || 'unknown'}
</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
)}
{result.assetInventory.kernelAssets?.kernelParameters && (
<Card size="small" title="关键内核参数">
<Descriptions size="small" column={1}>
{Object.entries(result.assetInventory.kernelAssets.kernelParameters).map(([key, value]) => (
<Descriptions.Item key={key} label={key}>
<code>{value}</code>
</Descriptions.Item>
))}
</Descriptions>
</Card>
)}
</Space>
),
},
]}
/>
</Card>
{/* 修复建议 */}
{result.recommendations && result.recommendations.length > 0 && (
<Card
title={<span className="font-semibold">修复建议</span>}
variant={'outlined'}
>
<List
dataSource={result.recommendations}
renderItem={(item, index) => (
<List.Item>
<Space align="start">
<span className="font-semibold text-gray-500">{index + 1}.</span>
<span className={
item.startsWith('【紧急】') ? 'text-red-600 font-medium' :
item.startsWith('【警告】') ? 'text-yellow-600' :
'text-gray-700'
}>
{item}
</span>
</Space>
</List.Item>
)}
/>
</Card>
{/* 采集警告 */}
{result.collectWarnings && result.collectWarnings.length > 0 && (
<Alert
type="warning"
message="采集警告"
description={
<ul className="list-disc pl-5">
{result.collectWarnings.map((warning, index) => (
<li key={index}>{warning}</li>
))}
</ul>
}
/>
)}
</div>
</Space>
);
};
+103 -103
View File
@@ -1,9 +1,9 @@
import {useEffect, useState} from 'react';
import {useNavigate} from 'react-router-dom';
import {App, Button, Divider, Form, Input} from 'antd';
import {LockOutlined, UserOutlined, GithubOutlined} from '@ant-design/icons';
import {Sparkles} from 'lucide-react';
import {getAuthConfig, getOIDCAuthURL, getGitHubAuthURL, login} from '../../api/auth';
import {App, Button, Form, Input} from 'antd';
import {GithubOutlined, GlobalOutlined, LockOutlined, UserOutlined} from '@ant-design/icons';
// 移除不需要的图标组件,保持轻量
import {getAuthConfig, getGitHubAuthURL, getOIDCAuthURL, login} from '../../api/auth';
import type {LoginRequest} from '../../types';
const Login = () => {
@@ -15,7 +15,6 @@ const Login = () => {
const navigate = useNavigate();
const {message: messageApi} = App.useApp();
// 获取认证配置
useEffect(() => {
fetchAuthConfig();
}, []);
@@ -35,15 +34,12 @@ const Login = () => {
try {
const response = await login(values);
const {token, user} = response.data;
// 保存 token 和用户信息
localStorage.setItem('token', token);
localStorage.setItem('userInfo', JSON.stringify(user));
messageApi.success('登录成功');
messageApi.success('欢迎回来');
navigate('/admin/agents');
} catch (error: any) {
messageApi.error(error.response?.data?.message || '登录失败,请检查用户名和密码');
messageApi.error(error.response?.data?.message || '账号或密码错误');
} finally {
setLoading(false);
}
@@ -53,11 +49,9 @@ const Login = () => {
setOidcLoading(true);
try {
const response = await getOIDCAuthURL();
const {authUrl} = response.data;
// 跳转到 OIDC 认证页面
window.location.href = authUrl;
window.location.href = response.data.authUrl;
} catch (error: any) {
messageApi.error(error.response?.data?.message || '获取 OIDC 认证地址失败');
messageApi.error('OIDC 跳转失败');
setOidcLoading(false);
}
};
@@ -66,111 +60,117 @@ const Login = () => {
setGithubLoading(true);
try {
const response = await getGitHubAuthURL();
const {authUrl} = response.data;
// 跳转到 GitHub 认证页面
window.location.href = authUrl;
window.location.href = response.data.authUrl;
} catch (error: any) {
messageApi.error(error.response?.data?.message || '获取 GitHub 认证地址失败');
messageApi.error('GitHub 跳转失败');
setGithubLoading(false);
}
};
return (
<div className="min-h-screen bg-slate-950">
<div className="mx-auto flex min-h-screen max-w-4xl items-center justify-center px-4 py-10">
<div className="relative w-full max-w-lg rounded-3xl border border-white/10 bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900 p-1">
<div className="rounded-[26px] bg-white p-8 shadow-2xl sm:p-10">
<div className="mb-8 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-slate-100 px-4 py-1 text-xs font-medium text-slate-600">
<Sparkles className="h-4 w-4 text-blue-600"/>
Pika Monitor
// 1. 背景改为柔和的灰白色,视觉更轻盈
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4">
{/* 2. 卡片去除复杂的渐变边框,使用简单的圆角和优雅的阴影 */}
<div className="w-full max-w-[400px] bg-white p-8 sm:p-10 rounded-2xl shadow-xl ring-1 ring-slate-900/5">
{/* 3. 头部精简:去掉了 Badge,强调品牌名称 */}
<div className="mb-10 text-center">
<h1 className="text-2xl font-bold tracking-tight text-slate-900">
{window.SystemConfig.SystemNameZh}
</h1>
<p className="mt-2 text-sm text-slate-500">
保持洞察,稳定运行
</p>
</div>
<Form
name="login"
layout="vertical"
size="large"
onFinish={onFinish}
autoComplete="off"
requiredMark={false} // 4. 隐藏必填星号,界面更干净
>
<Form.Item
name="username"
rules={[{required: true, message: '请输入用户名'}]}
className="mb-4"
>
<Input
prefix={<UserOutlined className="text-slate-400 mr-1"/>}
placeholder="用户名"
className="rounded-xl px-4 py-2.5 bg-slate-50 border-slate-200 hover:bg-white focus:bg-white transition-all"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{required: true, message: '请输入密码'}]}
className="mb-6"
>
<Input.Password
prefix={<LockOutlined className="text-slate-400 mr-1"/>}
placeholder="密码"
className="rounded-xl px-4 py-2.5 bg-slate-50 border-slate-200 hover:bg-white focus:bg-white transition-all"
/>
</Form.Item>
<Form.Item className="mb-0">
<Button
type="primary"
htmlType="submit"
loading={loading}
block
className="h-11 rounded-xl bg-slate-900 hover:bg-slate-800 font-medium shadow-sm transition-all"
>
登 录
</Button>
</Form.Item>
</Form>
{/* 5. 第三方登录区域 */}
{(oidcEnabled || githubEnabled) && (
<div className="mt-8">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-slate-200"/>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-slate-400">或者</span>
</div>
<h1 className="mt-4 text-3xl font-semibold text-slate-900">登录 Pika 控制台</h1>
<p className="mt-2 text-sm text-slate-500">面向探针的统一监控后台</p>
</div>
<Form
name="login"
layout="vertical"
size="large"
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
label="用户名"
name="username"
rules={[{required: true, message: '请输入用户名'}]}
>
<Input
prefix={<UserOutlined/>}
placeholder="请输入用户名"
/>
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{required: true, message: '请输入密码'}]}
>
<Input.Password
prefix={<LockOutlined/>}
placeholder="请输入密码"
/>
</Form.Item>
<Form.Item className="mb-0">
{/* 使用 Grid 布局让按钮并排,减少垂直高度占用 */}
<div className="mt-6 grid grid-cols-2 gap-3">
{githubEnabled && (
<Button
type="primary"
htmlType="submit"
loading={loading}
block
size="large"
className="h-12 rounded-2xl text-base font-semibold"
loading={githubLoading}
icon={<GithubOutlined/>}
onClick={handleGitHubLogin}
className={`h-10 rounded-xl border-slate-200 text-slate-700 font-medium hover:border-slate-300 hover:text-slate-900 ${!oidcEnabled ? 'col-span-2' : ''}`}
>
登录
GitHub
</Button>
</Form.Item>
</Form>
{(oidcEnabled || githubEnabled) && (
<div className="mt-8">
<Divider plain className="text-xs text-slate-400">其他登录方式</Divider>
<div className="mt-4 space-y-3">
{githubEnabled && (
<Button
block
loading={githubLoading}
icon={<GithubOutlined/>}
onClick={handleGitHubLogin}
size="large"
className="h-12 rounded-2xl border-slate-200 text-slate-800 hover:border-slate-300"
>
GitHub 登录
</Button>
)}
{oidcEnabled && (
<Button
block
loading={oidcLoading}
onClick={handleOIDCLogin}
size="large"
className="h-12 rounded-2xl border-slate-200 text-slate-800 hover:border-slate-300"
>
OIDC 登录
</Button>
)}
</div>
</div>
)}
<p className="mt-8 text-center text-xs text-slate-400">
登录即表示同意平台安全策略
</p>
)}
{oidcEnabled && (
<Button
block
loading={oidcLoading}
icon={<GlobalOutlined/>} // 换了一个更通用的图标
onClick={handleOIDCLogin}
className={`h-10 rounded-xl border-slate-200 text-slate-700 font-medium hover:border-slate-300 hover:text-slate-900 ${!githubEnabled ? 'col-span-2' : ''}`}
>
OIDC
</Button>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default Login;
export default Login;
+43 -31
View File
@@ -3,9 +3,9 @@ import {App, Button, Card, Form, Input, Space, Spin, Upload} from 'antd';
import {Upload as UploadIcon} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import type {SystemConfig} from '../../api/system-config';
import {getSystemConfig, saveSystemConfig, DEFAULT_SYSTEM_CONFIG} from '../../api/system-config';
import {getSystemConfig, saveSystemConfig} from '../../api/system-config';
import {getErrorMessage} from '../../lib/utils';
import type {RcFile, UploadFile} from 'antd/es/upload/interface';
import type {RcFile} from 'antd/es/upload/interface';
const SystemConfigComponent = () => {
const [form] = Form.useForm();
@@ -40,15 +40,11 @@ const SystemConfigComponent = () => {
form.setFieldsValue({
systemNameEn: config.systemNameEn,
systemNameZh: config.systemNameZh,
icpCode: config.icpCode,
});
if (config.logoBase64) {
setLogoPreview(config.logoBase64);
}
} else {
form.setFieldsValue({
systemNameEn: DEFAULT_SYSTEM_CONFIG.systemNameEn,
systemNameZh: DEFAULT_SYSTEM_CONFIG.systemNameZh,
});
}
}, [config, form]);
@@ -100,6 +96,7 @@ const SystemConfigComponent = () => {
systemNameEn: values.systemNameEn,
systemNameZh: values.systemNameZh,
logoBase64: logoPreview,
icpCode: values.icpCode || '',
} as SystemConfig);
} catch (error) {
// 表单验证失败
@@ -107,11 +104,15 @@ const SystemConfigComponent = () => {
};
const handleReset = () => {
form.setFieldsValue({
systemNameEn: DEFAULT_SYSTEM_CONFIG.systemNameEn,
systemNameZh: DEFAULT_SYSTEM_CONFIG.systemNameZh,
});
setLogoPreview('');
// 重置为当前配置的值
if (config) {
form.setFieldsValue({
systemNameEn: config.systemNameEn,
systemNameZh: config.systemNameZh,
icpCode: config.icpCode,
});
setLogoPreview(config.logoBase64 || '');
}
};
// 获取 Logo 显示 URL
@@ -144,28 +145,39 @@ const SystemConfigComponent = () => {
type="inner"
className="mb-4"
>
<Form.Item
label="系统英文名称"
name="systemNameEn"
rules={[
{required: true, message: '请输入系统英文名称'},
{max: 50, message: '系统名称不能超过 50 个字符'},
]}
tooltip="系统英文名称将显示在页面头部"
>
<Input placeholder="例如:Pika Monitor"/>
</Form.Item>
<div className={'flex items-center gap-2'}>
<Form.Item
label="系统英文名称"
name="systemNameEn"
rules={[
{required: true, message: '请输入系统英文名称'},
{max: 50, message: '系统名称不能超过 50 个字符'},
]}
>
<Input placeholder="例如:Pika Monitor"/>
</Form.Item>
<Form.Item
label="系统中文名称"
name="systemNameZh"
rules={[
{required: true, message: '请输入系统中文名称'},
{max: 50, message: '系统名称不能超过 50 个字符'},
]}
>
<Input placeholder="例如:皮卡监控"/>
</Form.Item>
</div>
<Form.Item
label="系统中文名称"
name="systemNameZh"
label="ICP 备案号"
name="icpCode"
rules={[
{required: true, message: '请输入系统中文名称'},
{max: 50, message: '系统名称不能超过 50 个字符'},
{max: 50, message: 'ICP 备案号不能超过 50 个字符'},
]}
tooltip="系统中文名称将显示在页面头部"
tooltip="ICP 备案号将显示在公共页面底部,例如:京ICP备12345678号"
>
<Input placeholder="例如:皮卡监控"/>
<Input placeholder="例如:京ICP备12345678号"/>
</Form.Item>
<Form.Item
@@ -205,8 +217,8 @@ const SystemConfigComponent = () => {
>
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => {
const systemNameEn = getFieldValue('systemNameEn') || DEFAULT_SYSTEM_CONFIG.systemNameEn;
const systemNameZh = getFieldValue('systemNameZh') || DEFAULT_SYSTEM_CONFIG.systemNameZh;
const systemNameEn = getFieldValue('systemNameEn') || '';
const systemNameZh = getFieldValue('systemNameZh') || '';
return (
<div className="flex items-center gap-3 p-4 bg-slate-50 rounded-lg">
<img