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) } }) }