优化安全审计

This commit is contained in:
dushixiang committed 2025-12-02 21:55:15 +08:00
1 parent 8f176e207e
commit 950d958dc2
9 files changed
+98 -103

No files matched your search

+2
View File
@@ -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
+4
View File
@@ -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=
+8
View File
@@ -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)
}
+12 -10
View File
@@ -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密钥信息
+49 -1
View File
@@ -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")
+1
View File
@@ -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
+5 -1
View File
@@ -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)
+2
View File
@@ -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;
}
+15 -91
View File
@@ -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<string, number> = {};
const privateIPCount: Record<string, number> = {};
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 <CheckCircle size={16} className="text-green-500"/>;
case 'fail':
return <XCircle size={16} className="text-red-500"/>;
case 'warn':
return <AlertTriangle size={16} className="text-yellow-500"/>;
case 'skip':
return <MinusCircle size={16} className="text-gray-400"/>;
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 <Tag color={config.color}>{config.text}</Tag>;
};
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 ? <Tag color={config.color}>{config.text}</Tag> : 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 (
<Space direction="vertical" style={{width: '100%'}}>
<Space direction="vertical" style={{width: '100%'}}>
{/* 风险概览 */}
{risks.length > 0 && (
<Card
@@ -877,7 +791,12 @@ const AuditResultView = ({result}: AuditResultViewProps) => {
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']
}}
/>
) : (
<Empty description="无登录历史"/>
@@ -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']
}}
/>
) : (
<Empty description="无失败登录记录"/>