优化安全审计
This commit is contained in:
1 parent
8f176e207e
commit
950d958dc2
9 files changed
+98
-103
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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密钥信息
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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="无失败登录记录"/>
|
||||
|
||||
Reference in new issue
Block a user