292 lines
9.7 KiB
Go
292 lines
9.7 KiB
Go
package oci
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/oracle/oci-go-sdk/v65/audit"
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
)
|
|
|
|
// maxAuditPages 限制单次查询的翻页数:繁忙租户单日事件可上千,
|
|
// 到限即返回 Truncated=true,由调用方收窄时间窗。
|
|
// 默认过滤(噪声事件/内网发起)后有效结果变少,页数放宽到 10 缓解截断。
|
|
const maxAuditPages = 10
|
|
|
|
// AuditEvent 是审计事件的列表精简视图;EventId 为 CloudEvents 全局唯一 id,
|
|
// 详情反查的键。Raw 为 SDK 原始事件的 JSON 序列化,由 service 层剥离进缓存,
|
|
// 列表响应不再携带(详情接口按 eventId 取回)。
|
|
type AuditEvent struct {
|
|
EventId string `json:"eventId"`
|
|
EventTime *time.Time `json:"eventTime"`
|
|
EventName string `json:"eventName"`
|
|
Source string `json:"source"`
|
|
ResourceName string `json:"resourceName"`
|
|
CompartmentName string `json:"compartmentName"`
|
|
PrincipalName string `json:"principalName"`
|
|
IPAddress string `json:"ipAddress"`
|
|
Status string `json:"status"`
|
|
RequestAction string `json:"requestAction"`
|
|
RequestPath string `json:"requestPath"`
|
|
Raw json.RawMessage `json:"raw,omitempty"`
|
|
}
|
|
|
|
// AuditEventsResult 是一次审计查询的结果;Truncated 表示翻页到限被截断,
|
|
// 此时 NextPage 携带 opc-next-page 游标,同一时间窗回传可断点续翻。
|
|
type AuditEventsResult struct {
|
|
Items []AuditEvent `json:"items"`
|
|
Truncated bool `json:"truncated"`
|
|
NextPage string `json:"nextPage,omitempty"`
|
|
}
|
|
|
|
func (c *RealClient) auditClient(cred Credentials, region string) (audit.AuditClient, error) {
|
|
ac, err := audit.NewAuditClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return ac, fmt.Errorf("new audit client: %w", err)
|
|
}
|
|
applyProxy(&ac.BaseClient, cred)
|
|
if region != "" {
|
|
ac.SetRegion(normalizeRegion(region))
|
|
}
|
|
return ac, nil
|
|
}
|
|
|
|
// ListAuditEvents 实现 Client:实时查询租户根 compartment 在 [start, end) 内的
|
|
// 审计事件,最多翻 maxAuditPages 页,结果按发生时间倒序;纯读不落库。
|
|
// page 非空时从该游标断点续翻(必须配同一时间窗);到限截断时回传 NextPage。
|
|
func (c *RealClient) ListAuditEvents(ctx context.Context, cred Credentials, region string, start, end time.Time, page string) (AuditEventsResult, error) {
|
|
ac, err := c.auditClient(cred, region)
|
|
if err != nil {
|
|
return AuditEventsResult{}, err
|
|
}
|
|
// Audit API 只接受分钟粒度:起止时间的秒与毫秒必须为 0
|
|
req := audit.ListEventsRequest{
|
|
CompartmentId: &cred.TenancyOCID,
|
|
StartTime: &common.SDKTime{Time: start.UTC().Truncate(time.Minute)},
|
|
EndTime: &common.SDKTime{Time: end.UTC().Truncate(time.Minute)},
|
|
}
|
|
if page != "" {
|
|
req.Page = &page
|
|
}
|
|
result := AuditEventsResult{Items: []AuditEvent{}}
|
|
for i := 0; i < maxAuditPages; i++ {
|
|
resp, err := ac.ListEvents(ctx, req)
|
|
if err != nil {
|
|
return AuditEventsResult{}, fmt.Errorf("list audit events: %w", err)
|
|
}
|
|
appendAuditEvents(&result, resp.Items)
|
|
if resp.OpcNextPage == nil {
|
|
sortAuditEvents(result.Items)
|
|
return result, nil
|
|
}
|
|
req.Page = resp.OpcNextPage
|
|
}
|
|
result.Truncated = true
|
|
result.NextPage = deref(req.Page)
|
|
sortAuditEvents(result.Items)
|
|
return result, nil
|
|
}
|
|
|
|
// appendAuditEvents 过滤噪声后追加一页事件;原始事件只对保留条目序列化。
|
|
func appendAuditEvents(result *AuditEventsResult, items []audit.AuditEvent) {
|
|
result.Items = appendKeptAuditEvents(result.Items, items)
|
|
}
|
|
|
|
// appendKeptAuditEvents 是过滤追加的通用形态,窗口式与批式查询共用。
|
|
func appendKeptAuditEvents(dst []AuditEvent, items []audit.AuditEvent) []AuditEvent {
|
|
for _, ev := range items {
|
|
out := toAuditEvent(ev)
|
|
if !keepAuditEvent(out) {
|
|
continue
|
|
}
|
|
if raw, mErr := json.Marshal(ev); mErr == nil {
|
|
out.Raw = raw
|
|
}
|
|
dst = append(dst, out)
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// ---- 批式懒加载查询:分窗回溯 + 游标续查 ----
|
|
|
|
// 批式查询参数:单批 OCI 翻页预算沿用 maxAuditPages;首窗 24h,
|
|
// 连续空窗倍增(上限 30 天)加速跨越闲置期;回溯下限为事件保留期 365 天。
|
|
const (
|
|
auditWindowHours = 24
|
|
auditWindowMaxHours = 720
|
|
auditRetentionDays = 365
|
|
)
|
|
|
|
// AuditCursor 是批式查询的续查位置:当前时间窗、窗内 OCI 翻页游标
|
|
// 与当前窗宽(小时,空窗倍增的记忆)。序列化为不透明 cursor 由 service 层负责。
|
|
type AuditCursor struct {
|
|
Start time.Time `json:"s"`
|
|
End time.Time `json:"e"`
|
|
Page string `json:"p,omitempty"`
|
|
WindowHours int `json:"w"`
|
|
}
|
|
|
|
// NewAuditCursor 构造首查游标:自 now 起回溯第一个 24h 窗。
|
|
func NewAuditCursor(now time.Time) AuditCursor {
|
|
end := now.UTC().Truncate(time.Minute)
|
|
return AuditCursor{Start: end.Add(-auditWindowHours * time.Hour), End: end, WindowHours: auditWindowHours}
|
|
}
|
|
|
|
// advance 推进到紧邻更早的窗;empty 表示刚结束的窗无保留事件,窗宽倍增,
|
|
// 否则重置 24h。done 为 true 表示已越过保留期尽头。
|
|
func (cur AuditCursor) advance(now time.Time, empty bool) (AuditCursor, bool) {
|
|
w := cur.WindowHours
|
|
if w <= 0 {
|
|
w = auditWindowHours
|
|
}
|
|
if empty {
|
|
if w *= 2; w > auditWindowMaxHours {
|
|
w = auditWindowMaxHours
|
|
}
|
|
} else {
|
|
w = auditWindowHours
|
|
}
|
|
end := cur.Start
|
|
if end.Before(now.UTC().AddDate(0, 0, -auditRetentionDays)) {
|
|
return cur, true
|
|
}
|
|
return AuditCursor{Start: end.Add(-time.Duration(w) * time.Hour), End: end, WindowHours: w}, false
|
|
}
|
|
|
|
// AuditBatchResult 是一批懒加载结果;Cursor 为 nil 且 Exhausted 为 true
|
|
// 表示已回溯到保留期尽头,无更早数据。
|
|
type AuditBatchResult struct {
|
|
Items []AuditEvent
|
|
Cursor *AuditCursor
|
|
Exhausted bool
|
|
}
|
|
|
|
// ListAuditEventsBatch 实现 Client:从 cur 位置向更早方向收集约 limit 条
|
|
// 保留事件;单批最多消费 maxAuditPages 页 OCI 调用,不足额也按预算返回,
|
|
// 由前端按需续查。窗口不重叠 + 窗内游标续翻保证跨批不重不漏。
|
|
func (c *RealClient) ListAuditEventsBatch(ctx context.Context, cred Credentials, region string, cur AuditCursor, limit int) (AuditBatchResult, error) {
|
|
ac, err := c.auditClient(cred, region)
|
|
if err != nil {
|
|
return AuditBatchResult{}, err
|
|
}
|
|
res := AuditBatchResult{Items: []AuditEvent{}}
|
|
windowHasKept := false
|
|
for budget := maxAuditPages; budget > 0 && len(res.Items) < limit; budget-- {
|
|
items, next, err := listAuditPage(ctx, ac, cred.TenancyOCID, cur)
|
|
if err != nil {
|
|
return AuditBatchResult{}, err
|
|
}
|
|
before := len(res.Items)
|
|
res.Items = appendKeptAuditEvents(res.Items, items)
|
|
windowHasKept = windowHasKept || len(res.Items) > before
|
|
if next != "" {
|
|
cur.Page = next
|
|
continue
|
|
}
|
|
nextCur, done := cur.advance(time.Now(), !windowHasKept)
|
|
if done {
|
|
res.Exhausted = true
|
|
sortAuditEvents(res.Items)
|
|
return res, nil
|
|
}
|
|
cur, windowHasKept = nextCur, false
|
|
}
|
|
sortAuditEvents(res.Items)
|
|
res.Cursor = &cur
|
|
return res, nil
|
|
}
|
|
|
|
// listAuditPage 拉取当前游标位置的一页原始事件。
|
|
func listAuditPage(ctx context.Context, ac audit.AuditClient, tenancyOCID string, cur AuditCursor) ([]audit.AuditEvent, string, error) {
|
|
req := audit.ListEventsRequest{
|
|
CompartmentId: &tenancyOCID,
|
|
StartTime: &common.SDKTime{Time: cur.Start.UTC().Truncate(time.Minute)},
|
|
EndTime: &common.SDKTime{Time: cur.End.UTC().Truncate(time.Minute)},
|
|
}
|
|
if cur.Page != "" {
|
|
req.Page = &cur.Page
|
|
}
|
|
resp, err := ac.ListEvents(ctx, req)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("list audit events: %w", err)
|
|
}
|
|
return resp.Items, deref(resp.OpcNextPage), nil
|
|
}
|
|
|
|
// auditInternalCIDRs 是 OCI 服务内部互调的发起方网段(RFC1918 + CGNAT)。
|
|
var auditInternalCIDRs = func() []*net.IPNet {
|
|
out := make([]*net.IPNet, 0, 4)
|
|
for _, cidr := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10"} {
|
|
_, block, _ := net.ParseCIDR(cidr)
|
|
out = append(out, block)
|
|
}
|
|
return out
|
|
}()
|
|
|
|
// keepAuditEvent 保留有展示价值的事件:Audit API 无服务端过滤参数(仅时间窗),
|
|
// 在翻页循环内排除高频遥测噪声(SummarizeMetricsData)与内网地址发起的服务互调;
|
|
// 无 IP 的事件(控制面内部)保留。
|
|
func keepAuditEvent(ev AuditEvent) bool {
|
|
if ev.EventName == "SummarizeMetricsData" {
|
|
return false
|
|
}
|
|
if ev.IPAddress == "" {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(ev.IPAddress)
|
|
if ip == nil {
|
|
return true
|
|
}
|
|
for _, block := range auditInternalCIDRs {
|
|
if block.Contains(ip) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// toAuditEvent 把 SDK 审计事件压平为列表 DTO;SDK 字段全为指针,逐层判 nil。
|
|
func toAuditEvent(ev audit.AuditEvent) AuditEvent {
|
|
out := AuditEvent{EventId: deref(ev.EventId), Source: deref(ev.Source)}
|
|
if ev.EventTime != nil {
|
|
out.EventTime = &ev.EventTime.Time
|
|
}
|
|
if ev.Data == nil {
|
|
return out
|
|
}
|
|
out.EventName = deref(ev.Data.EventName)
|
|
out.ResourceName = deref(ev.Data.ResourceName)
|
|
out.CompartmentName = deref(ev.Data.CompartmentName)
|
|
if id := ev.Data.Identity; id != nil {
|
|
out.PrincipalName = deref(id.PrincipalName)
|
|
out.IPAddress = deref(id.IpAddress)
|
|
}
|
|
if req := ev.Data.Request; req != nil {
|
|
out.RequestAction = deref(req.Action)
|
|
out.RequestPath = deref(req.Path)
|
|
}
|
|
if resp := ev.Data.Response; resp != nil {
|
|
out.Status = deref(resp.Status)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sortAuditEvents 按发生时间倒序排列;服务端返回顺序不保证,nil 时间排最后。
|
|
func sortAuditEvents(items []AuditEvent) {
|
|
sort.SliceStable(items, func(i, j int) bool {
|
|
ti, tj := items[i].EventTime, items[j].EventTime
|
|
switch {
|
|
case ti == nil:
|
|
return false
|
|
case tj == nil:
|
|
return true
|
|
default:
|
|
return ti.After(*tj)
|
|
}
|
|
})
|
|
}
|