From 950d958dc2884cd2436a14efca8ad95246363cff Mon Sep 17 00:00:00 2001 From: dushixiang Date: Tue, 2 Dec 2025 21:55:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=89=E5=85=A8=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 + go.sum | 4 + internal/config/config.go | 8 ++ internal/protocol/message.go | 22 ++--- internal/service/agent_service.go | 50 ++++++++++- internal/wire.go | 1 + internal/wire_gen.go | 6 +- web/src/api/agent.ts | 2 + web/src/pages/Agents/AuditResultView.tsx | 106 ++++------------------- 9 files changed, 98 insertions(+), 103 deletions(-) diff --git a/go.mod b/go.mod index 7c723ee..3b42fd0 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,8 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/oschwald/geoip2-golang v1.13.0 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect diff --git a/go.sum b/go.sum index 72e6780..79d9da9 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,10 @@ github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDw github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/config/config.go b/internal/config/config.go index 50bd2df..f0916d7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ type AppConfig struct { Users map[string]string `json:"Users"` // 用户名 -> bcrypt加密的密码 OIDC *OIDCConfig `json:"OIDC"` // OIDC配置(可选) GitHub *GitHubOAuthConfig `json:"GitHub"` // GitHub OAuth配置(可选) + GeoIP *GeoIPConfig `json:"GeoIP"` // GeoIP配置(可选) } // JWTConfig JWT配置 @@ -31,3 +32,10 @@ type GitHubOAuthConfig struct { RedirectURL string `json:"RedirectURL"` // 回调URL AllowedUsers []string `json:"AllowedUsers"` // 允许登录的GitHub用户名白名单(为空则允许所有用户) } + +// GeoIPConfig GeoIP配置 +type GeoIPConfig struct { + Enabled bool `json:"Enabled"` // 是否启用GeoIP查询 + DBPath string `json:"DBPath"` // GeoIP数据库文件路径(如:GeoLite2-City.mmdb) + DBLanguage string `json:"DBLanguage"` // 数据库语言(如:zh-CN、en) +} diff --git a/internal/protocol/message.go b/internal/protocol/message.go index cb3fe0e..404c307 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -501,20 +501,22 @@ type UserInfo struct { // 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 + Username string `json:"username"` // 用户名 + IP string `json:"ip,omitempty"` // IP地址 + Location string `json:"location,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"` // 空闲时间(秒) + Username string `json:"username"` // 用户名 + Terminal string `json:"terminal"` // 终端 + IP string `json:"ip"` // IP地址 + Location string `json:"location,omitempty"` // IP归属地 + LoginTime int64 `json:"loginTime"` // 登录时间(毫秒) + IdleTime int `json:"idleTime"` // 空闲时间(秒) } // SSHKeyInfo SSH密钥信息 diff --git a/internal/service/agent_service.go b/internal/service/agent_service.go index 88ccbca..cac7da4 100644 --- a/internal/service/agent_service.go +++ b/internal/service/agent_service.go @@ -22,9 +22,10 @@ type AgentService struct { monitorStatsRepo *repo.MonitorStatsRepo apiKeyService *ApiKeyService metricService *MetricService + geoipService *GeoIPService } -func NewAgentService(logger *zap.Logger, db *gorm.DB, apiKeyService *ApiKeyService, metricService *MetricService) *AgentService { +func NewAgentService(logger *zap.Logger, db *gorm.DB, apiKeyService *ApiKeyService, metricService *MetricService, geoipService *GeoIPService) *AgentService { return &AgentService{ logger: logger, Service: orz.NewService(db), @@ -32,6 +33,7 @@ func NewAgentService(logger *zap.Logger, db *gorm.DB, apiKeyService *ApiKeyServi monitorStatsRepo: repo.NewMonitorStatsRepo(db), apiKeyService: apiKeyService, metricService: metricService, + geoipService: geoipService, } } @@ -210,6 +212,9 @@ func (s *AgentService) handleVPSAuditResponse(ctx context.Context, agentID strin // SaveAuditResult 保存审计结果 func (s *AgentService) SaveAuditResult(ctx context.Context, agentID string, result *protocol.VPSAuditResult) error { + // 为登录记录添加 IP 归属地信息 + s.enrichLoginRecordsWithLocation(result) + // 将结果序列化为JSON存储 resultJSON, err := json.Marshal(result) if err != nil { @@ -238,6 +243,49 @@ func (s *AgentService) SaveAuditResult(ctx context.Context, agentID string, resu return nil } +// enrichLoginRecordsWithLocation 为登录记录添加IP归属地信息 +func (s *AgentService) enrichLoginRecordsWithLocation(result *protocol.VPSAuditResult) { + if s.geoipService == nil { + return + } + + // 处理成功登录记录 + if result.AssetInventory.LoginAssets != nil { + for i := range result.AssetInventory.LoginAssets.SuccessfulLogins { + if result.AssetInventory.LoginAssets.SuccessfulLogins[i].IP != "" { + location := s.geoipService.LookupIP(result.AssetInventory.LoginAssets.SuccessfulLogins[i].IP) + result.AssetInventory.LoginAssets.SuccessfulLogins[i].Location = location + } + } + + // 处理失败登录记录 + for i := range result.AssetInventory.LoginAssets.FailedLogins { + if result.AssetInventory.LoginAssets.FailedLogins[i].IP != "" { + location := s.geoipService.LookupIP(result.AssetInventory.LoginAssets.FailedLogins[i].IP) + result.AssetInventory.LoginAssets.FailedLogins[i].Location = location + } + } + + // 处理当前登录会话 + for i := range result.AssetInventory.LoginAssets.CurrentSessions { + if result.AssetInventory.LoginAssets.CurrentSessions[i].IP != "" { + location := s.geoipService.LookupIP(result.AssetInventory.LoginAssets.CurrentSessions[i].IP) + result.AssetInventory.LoginAssets.CurrentSessions[i].Location = location + } + } + } + + // 处理用户资产中的当前登录 + if result.AssetInventory.UserAssets != nil && result.AssetInventory.UserAssets.CurrentLogins != nil { + for i := range result.AssetInventory.UserAssets.CurrentLogins { + if result.AssetInventory.UserAssets.CurrentLogins[i].IP != "" { + location := s.geoipService.LookupIP(result.AssetInventory.UserAssets.CurrentLogins[i].IP) + result.AssetInventory.UserAssets.CurrentLogins[i].Location = location + } + } + } +} + // GetAuditResult 获取最新的审计结果(原始数据) func (s *AgentService) GetAuditResult(ctx context.Context, agentID string) (*protocol.VPSAuditResult, error) { record, err := s.AgentRepo.GetLatestAuditResultByType(ctx, agentID, "vps_audit") diff --git a/internal/wire.go b/internal/wire.go index 2ae39b3..be8dfe0 100644 --- a/internal/wire.go +++ b/internal/wire.go @@ -28,6 +28,7 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App service.NewMonitorService, service.NewTamperService, service.NewMetricService, + service.NewGeoIPService, service.NewNotifier, // WebSocket Manager diff --git a/internal/wire_gen.go b/internal/wire_gen.go index aee7d0f..9eecf8d 100644 --- a/internal/wire_gen.go +++ b/internal/wire_gen.go @@ -28,7 +28,11 @@ func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*App apiKeyService := service.NewApiKeyService(logger, db) propertyService := service.NewPropertyService(logger, db) metricService := service.NewMetricService(logger, db, propertyService) - agentService := service.NewAgentService(logger, db, apiKeyService, metricService) + geoIPService, err := service.NewGeoIPService(logger, cfg) + if err != nil { + return nil, err + } + agentService := service.NewAgentService(logger, db, apiKeyService, metricService, geoIPService) manager := websocket.NewManager(logger) monitorService := service.NewMonitorService(logger, db, manager) tamperRepo := repo.NewTamperRepo(db) diff --git a/web/src/api/agent.ts b/web/src/api/agent.ts index 276c74a..2346d9b 100644 --- a/web/src/api/agent.ts +++ b/web/src/api/agent.ts @@ -242,6 +242,7 @@ export interface UserInfo { export interface LoginRecord { username: string; ip?: string; + location?: string; terminal: string; timestamp: number; status?: string; @@ -251,6 +252,7 @@ export interface LoginSession { username: string; terminal: string; ip: string; + location?: string; loginTime: number; idleTime: number; } diff --git a/web/src/pages/Agents/AuditResultView.tsx b/web/src/pages/Agents/AuditResultView.tsx index c1f2c70..1fe50f9 100644 --- a/web/src/pages/Agents/AuditResultView.tsx +++ b/web/src/pages/Agents/AuditResultView.tsx @@ -35,21 +35,6 @@ interface AuditResultViewProps { } const AuditResultView = ({result}: AuditResultViewProps) => { - // 判断是否为内网IP - const isPrivateIP = (ip: string): boolean => { - if (!ip || ip === 'localhost') return true; - - // IPv4 私有地址 - const privateRanges = [ - /^10\./, // 10.0.0.0/8 - /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 - /^192\.168\./, // 192.168.0.0/16 - /^127\./, // 127.0.0.0/8 (loopback) - /^169\.254\./, // 169.254.0.0/16 (link-local) - ]; - - return privateRanges.some(range => range.test(ip)); - }; const analyzeRisks = (result: VPSAuditResult): RiskItem[] => { const risks: RiskItem[] = []; @@ -162,84 +147,11 @@ const AuditResultView = ({result}: AuditResultViewProps) => { } } - // 7. High Frequency Login IPs (区分内外网) - if (result.assetInventory.loginAssets?.successfulLogins) { - const publicIPCount: Record = {}; - const privateIPCount: Record = {}; - - result.assetInventory.loginAssets.successfulLogins.forEach(login => { - if (!login.ip || login.ip === 'localhost') return; - - if (isPrivateIP(login.ip)) { - privateIPCount[login.ip] = (privateIPCount[login.ip] || 0) + 1; - } else { - publicIPCount[login.ip] = (publicIPCount[login.ip] || 0) + 1; - } - }); - - // 外网IP:超过20次就警告 - const highFreqPublicIPs = Object.entries(publicIPCount).filter(([_, count]) => count > 20); - if (highFreqPublicIPs.length > 0) { - risks.push({ - level: 'medium', - title: '发现高频外网IP登录', - description: `以下公网IP登录次数较多: ${highFreqPublicIPs.map(([ip, count]) => `${ip}(${count}次)`).join(', ')}` - }); - } - - // 内网IP:超过100次才警告(内网环境下频繁登录是正常的) - const highFreqPrivateIPs = Object.entries(privateIPCount).filter(([_, count]) => count > 100); - if (highFreqPrivateIPs.length > 0) { - risks.push({ - level: 'low', - title: '内网IP登录次数较多', - description: `以下内网IP登录次数较多: ${highFreqPrivateIPs.map(([ip, count]) => `${ip}(${count}次)`).join(', ')}` - }); - } - } - return risks; }; const risks = analyzeRisks(result); - const getStatusIcon = (status: string) => { - switch (status) { - case 'pass': - return ; - case 'fail': - return ; - case 'warn': - return ; - case 'skip': - return ; - default: - return null; - } - }; - - const getStatusTag = (status: string) => { - const configs = { - pass: {color: 'success', text: '通过'}, - fail: {color: 'error', text: '失败'}, - warn: {color: 'warning', text: '警告'}, - skip: {color: 'default', text: '跳过'}, - }; - const config = configs[status as keyof typeof configs] || {color: 'default', text: status}; - return {config.text}; - }; - - const getSeverityTag = (severity?: string) => { - if (!severity) return null; - const configs = { - high: {color: 'error', text: '高危'}, - medium: {color: 'warning', text: '中危'}, - low: {color: 'default', text: '低危'}, - }; - const config = configs[severity as keyof typeof configs]; - return config ? {config.text} : null; - }; - const getThreatLevelTag = (level: string) => { const configs = { critical: {color: 'error', text: '危急'}, @@ -338,6 +250,7 @@ const AuditResultView = ({result}: AuditResultViewProps) => { const loginRecordColumns = [ {title: '用户名', dataIndex: 'username', key: 'username', width: 120}, {title: 'IP地址', dataIndex: 'ip', key: 'ip', width: 150}, + {title: '归属地', dataIndex: 'location', key: 'location', width: 200, ellipsis: true}, {title: '终端', dataIndex: 'terminal', key: 'terminal', width: 100}, { title: '登录时间', @@ -358,6 +271,7 @@ const AuditResultView = ({result}: AuditResultViewProps) => { const currentSessionColumns = [ {title: '用户名', dataIndex: 'username', key: 'username', width: 120}, {title: 'IP地址', dataIndex: 'ip', key: 'ip', width: 150}, + {title: '归属地', dataIndex: 'location', key: 'location', width: 200, ellipsis: true}, {title: '终端', dataIndex: 'terminal', key: 'terminal', width: 100}, { title: '登录时间', @@ -466,7 +380,7 @@ const AuditResultView = ({result}: AuditResultViewProps) => { ]; return ( - + {/* 风险概览 */} {risks.length > 0 && ( { dataSource={result.assetInventory.loginAssets.successfulLogins} columns={loginRecordColumns} rowKey={(record, index) => `success-${index}`} - pagination={{pageSize: 20}} + pagination={{ + pageSize: 20, + showSizeChanger: true, + showTotal: (total) => `共 ${total} 条记录`, + pageSizeOptions: ['10', '20', '50', '100'] + }} /> ) : ( @@ -891,7 +810,12 @@ const AuditResultView = ({result}: AuditResultViewProps) => { dataSource={result.assetInventory.loginAssets.failedLogins} columns={loginRecordColumns} rowKey={(record, index) => `failed-${index}`} - pagination={{pageSize: 20}} + pagination={{ + pageSize: 20, + showSizeChanger: true, + showTotal: (total) => `共 ${total} 条记录`, + pageSizeOptions: ['10', '20', '50', '100'] + }} /> ) : (