feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
1 parent
e57718998a
commit
77c234b060
314 files changed
+10050
-4065
No files matched your search
@@ -0,0 +1,232 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AliyunProvider 阿里云 ECS Provider
|
||||
type AliyunProvider struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
version string // API 版本
|
||||
}
|
||||
|
||||
func NewAliyunProvider(accessKey, secretKey string) *AliyunProvider {
|
||||
return &AliyunProvider{
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
version: "2014-05-26",
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AliyunProvider) Name() string { return "aliyun" }
|
||||
|
||||
// percentEncode 阿里云 RPC 签名专用 URL 编码
|
||||
func percentEncode(s string) string {
|
||||
s = url.QueryEscape(s)
|
||||
s = strings.ReplaceAll(s, "+", "%20")
|
||||
s = strings.ReplaceAll(s, "*", "%2A")
|
||||
s = strings.ReplaceAll(s, "%7E", "~")
|
||||
return s
|
||||
}
|
||||
|
||||
// sign 计算阿里云 RPC v1 HMAC-SHA1 签名
|
||||
func (a *AliyunProvider) sign(params map[string]string) string {
|
||||
// 1. 排序参数 key
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 2. 拼接规范化查询字符串
|
||||
var pairs []string
|
||||
for _, k := range keys {
|
||||
pairs = append(pairs, percentEncode(k)+"="+percentEncode(params[k]))
|
||||
}
|
||||
canonicalized := strings.Join(pairs, "&")
|
||||
|
||||
// 3. 构造待签名字符串
|
||||
stringToSign := "GET&" + percentEncode("/") + "&" + percentEncode(canonicalized)
|
||||
|
||||
// 4. HMAC-SHA1
|
||||
mac := hmac.New(sha1.New, []byte(a.secretKey+"&"))
|
||||
mac.Write([]byte(stringToSign))
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// call 调用阿里云 RPC API
|
||||
func (a *AliyunProvider) call(action string, extra map[string]string, endpoint string) ([]byte, error) {
|
||||
if endpoint == "" {
|
||||
endpoint = "https://ecs.aliyuncs.com/"
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"Format": "JSON",
|
||||
"Version": a.version,
|
||||
"AccessKeyId": a.accessKey,
|
||||
"SignatureMethod": "HMAC-SHA1",
|
||||
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
"SignatureVersion": "1.0",
|
||||
"SignatureNonce": fmt.Sprintf("%d", rand.Int63()),
|
||||
"Action": action,
|
||||
}
|
||||
for k, v := range extra {
|
||||
params[k] = v
|
||||
}
|
||||
params["Signature"] = a.sign(params)
|
||||
|
||||
// 构造 GET URL
|
||||
var pairs []string
|
||||
for k, v := range params {
|
||||
pairs = append(pairs, url.QueryEscape(k)+"="+url.QueryEscape(v))
|
||||
}
|
||||
reqURL := endpoint + "?" + strings.Join(pairs, "&")
|
||||
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求阿里云 API 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("阿里云 API 返回错误 (HTTP %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// aliyunRegionsResponse DescribeRegions 响应
|
||||
type aliyunRegionsResponse struct {
|
||||
Regions struct {
|
||||
Region []struct {
|
||||
RegionID string `json:"RegionId"`
|
||||
LocalName string `json:"LocalName"`
|
||||
RegionEndpoint string `json:"RegionEndpoint"`
|
||||
} `json:"Region"`
|
||||
} `json:"Regions"`
|
||||
}
|
||||
|
||||
// ListRegions 获取阿里云所有区域
|
||||
func (a *AliyunProvider) ListRegions() ([]CloudRegion, error) {
|
||||
body, err := a.call("DescribeRegions", nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp aliyunRegionsResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("解析区域响应失败: %w", err)
|
||||
}
|
||||
|
||||
regions := make([]CloudRegion, 0, len(resp.Regions.Region))
|
||||
for _, r := range resp.Regions.Region {
|
||||
regions = append(regions, CloudRegion{
|
||||
RegionID: r.RegionID,
|
||||
LocalName: r.LocalName,
|
||||
Endpoint: r.RegionEndpoint,
|
||||
Status: "active",
|
||||
})
|
||||
}
|
||||
return regions, nil
|
||||
}
|
||||
|
||||
// aliyunInstancesResponse DescribeInstances 响应
|
||||
type aliyunInstancesResponse struct {
|
||||
TotalCount int `json:"TotalCount"`
|
||||
Instances struct {
|
||||
Instance []struct {
|
||||
InstanceID string `json:"InstanceId"`
|
||||
InstanceName string `json:"InstanceName"`
|
||||
HostName string `json:"HostName"`
|
||||
Status string `json:"Status"`
|
||||
RegionID string `json:"RegionId"`
|
||||
ZoneID string `json:"ZoneId"`
|
||||
InstanceType string `json:"InstanceType"`
|
||||
CPU int `json:"Cpu"`
|
||||
Memory int `json:"Memory"`
|
||||
OSType string `json:"OSType"`
|
||||
CreationTime string `json:"CreationTime"`
|
||||
|
||||
VPCAttributes struct {
|
||||
PrivateIPAddress struct {
|
||||
IPAddress []string `json:"IpAddress"`
|
||||
} `json:"PrivateIpAddress"`
|
||||
} `json:"VpcAttributes"`
|
||||
|
||||
PublicIPAddress struct {
|
||||
IPAddress []string `json:"IpAddress"`
|
||||
} `json:"PublicIpAddress"`
|
||||
|
||||
InnerIPAddress struct {
|
||||
IPAddress []string `json:"IpAddress"`
|
||||
} `json:"InnerIpAddress"`
|
||||
} `json:"Instance"`
|
||||
} `json:"Instances"`
|
||||
}
|
||||
|
||||
// ListInstances 获取指定区域的云主机实例
|
||||
func (a *AliyunProvider) ListInstances(regionID string) ([]CloudInstance, error) {
|
||||
endpoint := fmt.Sprintf("https://ecs.%s.aliyuncs.com/", regionID)
|
||||
|
||||
body, err := a.call("DescribeInstances", map[string]string{
|
||||
"RegionId": regionID,
|
||||
"PageSize": "100",
|
||||
}, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp aliyunInstancesResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("解析实例响应失败: %w", err)
|
||||
}
|
||||
|
||||
instances := make([]CloudInstance, 0, len(resp.Instances.Instance))
|
||||
for _, inst := range resp.Instances.Instance {
|
||||
privateIP := ""
|
||||
if len(inst.VPCAttributes.PrivateIPAddress.IPAddress) > 0 {
|
||||
privateIP = inst.VPCAttributes.PrivateIPAddress.IPAddress[0]
|
||||
}
|
||||
publicIP := ""
|
||||
if len(inst.PublicIPAddress.IPAddress) > 0 {
|
||||
publicIP = inst.PublicIPAddress.IPAddress[0]
|
||||
}
|
||||
osType := inst.OSType
|
||||
if osType == "" {
|
||||
osType = "linux"
|
||||
}
|
||||
|
||||
instances = append(instances, CloudInstance{
|
||||
InstanceID: inst.InstanceID,
|
||||
InstanceName: inst.InstanceName,
|
||||
Hostname: inst.HostName,
|
||||
OSType: osType,
|
||||
Status: inst.Status,
|
||||
RegionID: inst.RegionID,
|
||||
ZoneID: inst.ZoneID,
|
||||
PrivateIP: privateIP,
|
||||
PublicIP: publicIP,
|
||||
InstanceType: inst.InstanceType,
|
||||
CPU: inst.CPU,
|
||||
Memory: inst.Memory,
|
||||
CreationTime: inst.CreationTime,
|
||||
})
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
Reference in new issue
Block a user