优化代码
This commit is contained in:
1 parent
b446600dfe
commit
ab24de67f5
31 files changed
+2695
-2429
No files matched your search
+2
-19
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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"` // 安全启动状态
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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", ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
window.SystemConfig = {
|
||||
SystemNameZh: "{{.SystemNameZh}}",
|
||||
SystemNameEn: "{{.SystemNameEn}}",
|
||||
ICPCode: "{{.ICPCode}}",
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
+94
-12
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
// 保存系统配置
|
||||
|
||||
@@ -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>
|
||||
|
||||
Vendored
+1
@@ -5,6 +5,7 @@ declare global {
|
||||
SystemConfig: {
|
||||
SystemNameZh: string;
|
||||
SystemNameEn: string;
|
||||
ICPCode: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user