577 lines
21 KiB
Go
577 lines
21 KiB
Go
package oci
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/oracle/oci-go-sdk/v65/audit"
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
"github.com/oracle/oci-go-sdk/v65/loggingsearch"
|
|
)
|
|
|
|
// maxAuditPages 限制单次查询的翻页数:每页最多 auditSearchPageLimit 条,
|
|
// 到限即截断(窗口式回传 Truncated,批式留游标),由调用方续查。
|
|
const maxAuditPages = 10
|
|
|
|
// auditBatchTimeBudget 是批式查询的单批耗时预算:全文检索命中稀疏时
|
|
// 大窗扫描单页可达十余秒,超时即带游标返回,把长回溯拆成多个有界请求,
|
|
// 前端按已回溯位置展示进度并自动续查。
|
|
const auditBatchTimeBudget = 20 * time.Second
|
|
|
|
// auditSearchPageLimit 是 SearchLogs 单页条数(API 上限 1000):批式查询
|
|
// 攒满目标条数(~100)即携整页返回,页取 200 兼顾单页凑满一批与响应体量。
|
|
const auditSearchPageLimit = 200
|
|
|
|
// AuditEvent 是审计事件的列表精简视图;EventId 为 CloudEvents 全局唯一 id,
|
|
// 详情反查的键。Raw 为 _Audit 日志 logContent 原文,由 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"`
|
|
}
|
|
|
|
// auditSearchClient 构造区域化的日志搜索客户端。审计数据源为 Logging Search
|
|
// 的 _Audit 日志:Audit API 无排序参数、窗口内固定按处理时间正序,首批只能
|
|
// 拿到窗口内最旧的一段;Logging Search 支持 datetime 倒序,才能从最新回溯。
|
|
func (c *RealClient) auditSearchClient(cred Credentials, region string) (loggingsearch.LogSearchClient, error) {
|
|
sc, err := loggingsearch.NewLogSearchClientWithConfigurationProvider(provider(cred))
|
|
if err != nil {
|
|
return sc, fmt.Errorf("new logging search client: %w", err)
|
|
}
|
|
applyProxy(&sc.BaseClient, cred)
|
|
if region != "" {
|
|
sc.SetRegion(normalizeRegion(region))
|
|
}
|
|
return sc, nil
|
|
}
|
|
|
|
// auditSearchQuery 组装租户根 compartment 审计日志的倒序检索语句;
|
|
// SummarizeMetricsData 遥测噪声占比高,服务端先滤一道减少无效翻页。
|
|
// q 非空时追加 logContent 全文包含匹配——它扫的是整条 JSON 的所有值,只当
|
|
// 粗筛;可见字段的精筛由 filterAuditTerm 兜底,避免隐藏元数据误命中。
|
|
func auditSearchQuery(tenancyOCID, q string) string {
|
|
query := fmt.Sprintf("search %q | where data.eventName != 'SummarizeMetricsData'", tenancyOCID+"/_Audit")
|
|
if term := SanitizeAuditTerm(q); term != "" {
|
|
query += fmt.Sprintf(" and logContent = '*%s*'", term)
|
|
}
|
|
return query + " | sort by datetime desc"
|
|
}
|
|
|
|
// auditTermMaxLen 限制检索关键字长度,防止游标与查询语句被撑爆。
|
|
const auditTermMaxLen = 100
|
|
|
|
// SanitizeAuditTerm 归一检索关键字:去除引号/反斜杠/控制字符防语句注入
|
|
// (查询目标已锁定本租户 _Audit 流,注入最坏只是语法错),截断超长输入;
|
|
// 保留 * 供用户通配。返回空串表示不追加过滤子句。
|
|
// service 构造首查游标与本包组装语句共用,对篡改游标二次消毒兜底。
|
|
func SanitizeAuditTerm(q string) string {
|
|
out := make([]rune, 0, len(q))
|
|
for _, r := range q {
|
|
if r == '\'' || r == '"' || r == '\\' || r < 0x20 {
|
|
continue
|
|
}
|
|
out = append(out, r)
|
|
if len(out) >= auditTermMaxLen {
|
|
break
|
|
}
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// ListAuditEvents 实现 Client:实时查询租户根 compartment 在 [start, end) 内的
|
|
// 审计事件,最多翻 maxAuditPages 页,结果按发生时间倒序;纯读不落库。
|
|
// page 非空时从该游标断点续翻(必须配同一时间窗);到限截断时回传 NextPage。
|
|
// Search 配额为零的租户自动回退 Audit API 重查同一窗口(page 跨通道失效,重头吃窗)。
|
|
func (c *RealClient) ListAuditEvents(ctx context.Context, cred Credentials, region string, start, end time.Time, page string) (AuditEventsResult, error) {
|
|
f := c.newAuditFetchers(cred, region)
|
|
res, err := listAuditWindow(ctx, f.search, AuditCursor{Start: start, End: end, Page: page})
|
|
if err != nil && isSearchQuotaZero(err) {
|
|
res, err = listAuditWindow(ctx, f.audit, AuditCursor{Start: start, End: end})
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// listAuditWindow 用给定取页函数吃一个固定时间窗,最多 maxAuditPages 页;
|
|
// 页预算耗尽即截断,NextPage 携带未消费的窗内游标。
|
|
func listAuditWindow(ctx context.Context, fetch auditPageFetch, cur AuditCursor) (AuditEventsResult, error) {
|
|
result := AuditEventsResult{Items: []AuditEvent{}}
|
|
for i := 0; i < maxAuditPages; i++ {
|
|
items, next, err := fetch(ctx, cur)
|
|
if err != nil {
|
|
return AuditEventsResult{}, err
|
|
}
|
|
result.Items = appendKeptAuditEvents(result.Items, items)
|
|
if next == "" {
|
|
sortAuditEvents(result.Items)
|
|
return result, nil
|
|
}
|
|
cur.Page = next
|
|
}
|
|
result.Truncated = true
|
|
result.NextPage = cur.Page
|
|
sortAuditEvents(result.Items)
|
|
return result, nil
|
|
}
|
|
|
|
// auditPageFetch 拉取游标位置的一页已映射事件,返回窗内下一页游标。
|
|
type auditPageFetch func(ctx context.Context, cur AuditCursor) ([]AuditEvent, string, error)
|
|
|
|
// auditFetchers 汇集两条数据通道:search 为 Logging Search 倒序主路,
|
|
// audit 为 Search 配额为零租户的 Audit API 回退路。
|
|
type auditFetchers struct {
|
|
search auditPageFetch
|
|
audit auditPageFetch
|
|
}
|
|
|
|
// newAuditFetchers 构造两条通道的取页闭包;客户端惰性初始化,
|
|
// 各模式的续查不会白建用不到的客户端。
|
|
func (c *RealClient) newAuditFetchers(cred Credentials, region string) auditFetchers {
|
|
return auditFetchers{search: c.searchFetcher(cred, region), audit: c.auditAPIFetcher(cred, region)}
|
|
}
|
|
|
|
// searchFetcher 构造 Logging Search 通道的取页闭包。
|
|
func (c *RealClient) searchFetcher(cred Credentials, region string) auditPageFetch {
|
|
var sc *loggingsearch.LogSearchClient
|
|
return func(ctx context.Context, cur AuditCursor) ([]AuditEvent, string, error) {
|
|
if sc == nil {
|
|
cli, err := c.auditSearchClient(cred, region)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
sc = &cli
|
|
}
|
|
return searchAuditPage(ctx, *sc, cred.TenancyOCID, cur)
|
|
}
|
|
}
|
|
|
|
// auditAPIFetcher 构造 Audit API 回退通道的取页闭包。
|
|
func (c *RealClient) auditAPIFetcher(cred Credentials, region string) auditPageFetch {
|
|
var ac *audit.AuditClient
|
|
return func(ctx context.Context, cur AuditCursor) ([]AuditEvent, string, error) {
|
|
if ac == nil {
|
|
cli, err := c.auditClient(cred, region)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
ac = &cli
|
|
}
|
|
return listAuditPage(ctx, *ac, cred.TenancyOCID, cur)
|
|
}
|
|
}
|
|
|
|
// isSearchQuotaZero 识别「租户 Logging Search 配额为零」的失败:此类租户该
|
|
// 服务永久不可用(maxQueriesPerMinute/maxConcurrentQueries 均为 0),应回退
|
|
// Audit API;普通限流(配额非零)不回退,避免数据通道来回切换。
|
|
func isSearchQuotaZero(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := strings.ToLower(strings.ReplaceAll(err.Error(), " ", ""))
|
|
return strings.Contains(msg, "ratelimitexceeded") && strings.Contains(msg, "maxqueriesperminute:0,")
|
|
}
|
|
|
|
// appendKeptAuditEvents 过滤噪声后追加一页已映射事件;窗口式与批式查询共用。
|
|
func appendKeptAuditEvents(dst []AuditEvent, items []AuditEvent) []AuditEvent {
|
|
for _, ev := range items {
|
|
if keepAuditEvent(ev) {
|
|
dst = append(dst, ev)
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// ---- 批式懒加载查询:分窗回溯 + 游标续查 ----
|
|
|
|
// 批式查询参数:单批翻页预算沿用 maxAuditPages;首窗 24h,连续空窗倍增
|
|
// 加速跨越闲置期,上限 14 天(Logging Search 单次查询时间窗硬限);
|
|
// 回溯下限为审计事件保留期 365 天。
|
|
const (
|
|
auditWindowHours = 24
|
|
auditWindowMaxHours = 336
|
|
auditRetentionDays = 365
|
|
)
|
|
|
|
// auditModeFallback 标记游标处于 Audit API 回退模式:部分租户的
|
|
// Logging Search 服务配额为零(maxQueriesPerMinute: 0),永久不可用。
|
|
const auditModeFallback = "a"
|
|
|
|
// auditFallbackWindowHours 是回退模式的基准窗宽:Audit API 窗口内固定按
|
|
// 处理时间正序且无排序参数,只能小窗回溯 + 前端全局重排保住从新到旧的体验。
|
|
const auditFallbackWindowHours = 1
|
|
|
|
// AuditCursor 是批式查询的续查位置:当前时间窗、窗内翻页游标、当前窗宽
|
|
// (小时,空窗倍增的记忆)、检索关键字(随游标续查,保证跨批过滤一致)
|
|
// 与数据通道模式(空为 Search 主路,"a" 为 Audit API 回退,续查沿用不再试错)。
|
|
// 序列化为不透明 cursor 由 service 层负责。
|
|
type AuditCursor struct {
|
|
Start time.Time `json:"s"`
|
|
End time.Time `json:"e"`
|
|
Page string `json:"p,omitempty"`
|
|
WindowHours int `json:"w"`
|
|
Q string `json:"q,omitempty"`
|
|
M string `json:"m,omitempty"`
|
|
}
|
|
|
|
// toFallback 把游标切到 Audit API 回退模式:Search 页游标跨通道失效须清空;
|
|
// 首窗收窄到基准窗宽,避免大窗正序分页又回到「首批全是窗口内最旧事件」的老问题。
|
|
func (cur AuditCursor) toFallback() AuditCursor {
|
|
cur.M = auditModeFallback
|
|
cur.Page = ""
|
|
cur.WindowHours = auditFallbackWindowHours
|
|
if cur.End.Sub(cur.Start) > auditFallbackWindowHours*time.Hour {
|
|
cur.Start = cur.End.Add(-auditFallbackWindowHours * time.Hour)
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// 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 表示刚结束的窗无保留事件,窗宽倍增,
|
|
// 否则重置为该模式基准窗宽。done 为 true 表示已越过保留期尽头。
|
|
func (cur AuditCursor) advance(now time.Time, empty bool) (AuditCursor, bool) {
|
|
base := auditWindowHours
|
|
if cur.M == auditModeFallback {
|
|
base = auditFallbackWindowHours
|
|
}
|
|
w := cur.WindowHours
|
|
if w <= 0 {
|
|
w = base
|
|
}
|
|
if empty {
|
|
if w *= 2; w > auditWindowMaxHours {
|
|
w = auditWindowMaxHours
|
|
}
|
|
} else {
|
|
w = base
|
|
}
|
|
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, Q: cur.Q, M: cur.M}, false
|
|
}
|
|
|
|
// AuditBatchResult 是一批懒加载结果;Cursor 为 nil 且 Exhausted 为 true
|
|
// 表示已回溯到保留期尽头,无更早数据。
|
|
type AuditBatchResult struct {
|
|
Items []AuditEvent
|
|
Cursor *AuditCursor
|
|
Exhausted bool
|
|
}
|
|
|
|
// ListAuditEventsBatch 实现 Client:从 cur 位置向更早方向收集约 limit 条
|
|
// 保留事件;单批受页预算与时间预算双重约束,不足额也返回,由前端按需续查。
|
|
// 倒序返回下,窗口不重叠 + 窗内游标续翻保证跨批不重不漏。
|
|
func (c *RealClient) ListAuditEventsBatch(ctx context.Context, cred Credentials, region string, cur AuditCursor, limit int) (AuditBatchResult, error) {
|
|
return listAuditBatch(ctx, c.newAuditFetchers(cred, region), cur, limit)
|
|
}
|
|
|
|
// listAuditBatch 是批式回溯的通道无关内核,取页函数注入便于测试。
|
|
func listAuditBatch(ctx context.Context, f auditFetchers, cur AuditCursor, limit int) (AuditBatchResult, error) {
|
|
res := AuditBatchResult{Items: []AuditEvent{}}
|
|
windowHasKept := false
|
|
deadline := time.Now().Add(auditBatchTimeBudget)
|
|
for budget := maxAuditPages; budget > 0 && len(res.Items) < limit && time.Now().Before(deadline); budget-- {
|
|
items, next, nextCur, err := fetchAuditPage(ctx, f, cur)
|
|
if err != nil {
|
|
return AuditBatchResult{}, err
|
|
}
|
|
cur = nextCur
|
|
before := len(res.Items)
|
|
res.Items = appendKeptAuditEvents(res.Items, filterAuditTerm(items, cur))
|
|
windowHasKept = windowHasKept || len(res.Items) > before
|
|
if next != "" {
|
|
cur.Page = next
|
|
continue
|
|
}
|
|
adv, done := cur.advance(time.Now(), !windowHasKept)
|
|
if done {
|
|
res.Exhausted = true
|
|
sortAuditEvents(res.Items)
|
|
return res, nil
|
|
}
|
|
cur, windowHasKept = adv, false
|
|
}
|
|
sortAuditEvents(res.Items)
|
|
res.Cursor = &cur
|
|
return res, nil
|
|
}
|
|
|
|
// fetchAuditPage 按游标模式取一页;Search 主路报「配额为零」时切到回退游标
|
|
// 并立即用 Audit API 重试,后续批次凭游标模式直达回退通道不再试错。
|
|
func fetchAuditPage(ctx context.Context, f auditFetchers, cur AuditCursor) ([]AuditEvent, string, AuditCursor, error) {
|
|
if cur.M == auditModeFallback {
|
|
items, next, err := f.audit(ctx, cur)
|
|
return items, next, cur, err
|
|
}
|
|
items, next, err := f.search(ctx, cur)
|
|
if err != nil && isSearchQuotaZero(err) {
|
|
cur = cur.toFallback()
|
|
items, next, err = f.audit(ctx, cur)
|
|
}
|
|
return items, next, cur, err
|
|
}
|
|
|
|
// filterAuditTerm 关键字精筛:只认列表可见字段(matchesAuditTerm),两条通道
|
|
// 语义一致。Search 主路的 logContent 全文条件会命中隐藏认证元数据(如
|
|
// opc-principal 头里的 ttype:login),只作粗筛减少翻页,不作为最终判定。
|
|
func filterAuditTerm(items []AuditEvent, cur AuditCursor) []AuditEvent {
|
|
if cur.Q == "" {
|
|
return items
|
|
}
|
|
out := items[:0]
|
|
for _, ev := range items {
|
|
if matchesAuditTerm(ev, cur.Q) {
|
|
out = append(out, ev)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// matchesAuditTerm 判断事件是否命中关键字:不区分大小写的包含匹配,
|
|
// * 作为通配分段、各段都出现即命中,近似 Search 通道的 logContent 语义。
|
|
func matchesAuditTerm(ev AuditEvent, q string) bool {
|
|
hay := strings.ToLower(strings.Join([]string{
|
|
ev.EventName, ev.Source, ev.ResourceName, ev.CompartmentName,
|
|
ev.PrincipalName, ev.IPAddress, ev.Status, ev.RequestAction, ev.RequestPath,
|
|
}, "\n"))
|
|
for _, part := range strings.Split(strings.ToLower(q), "*") {
|
|
if part != "" && !strings.Contains(hay, part) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// auditClient 构造区域化的 Audit API 客户端(回退通道)。
|
|
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
|
|
}
|
|
|
|
// listAuditPage 拉取窗口内一页 Audit API 原始事件并压平;该 API 窗口内固定
|
|
// 正序且只接受分钟粒度(起止秒与毫秒必须为 0)。Raw 为 SDK 事件原文,
|
|
// 与 Search 通道的 logContent 形态不同,详情弹窗均按任意 JSON 渲染。
|
|
func listAuditPage(ctx context.Context, ac audit.AuditClient, tenancyOCID string, cur AuditCursor) ([]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)
|
|
}
|
|
items := make([]AuditEvent, 0, len(resp.Items))
|
|
for _, ev := range resp.Items {
|
|
out := toAuditEvent(ev)
|
|
if raw, mErr := json.Marshal(ev); mErr == nil {
|
|
out.Raw = raw
|
|
}
|
|
items = append(items, out)
|
|
}
|
|
return items, deref(resp.OpcNextPage), nil
|
|
}
|
|
|
|
// toAuditEvent 把 Audit 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
|
|
}
|
|
|
|
// searchAuditPage 拉取游标窗口内按 datetime 倒序的一页审计事件(已映射未过滤)。
|
|
func searchAuditPage(ctx context.Context, sc loggingsearch.LogSearchClient, tenancyOCID string, cur AuditCursor) ([]AuditEvent, string, error) {
|
|
req := loggingsearch.SearchLogsRequest{
|
|
SearchLogsDetails: loggingsearch.SearchLogsDetails{
|
|
TimeStart: &common.SDKTime{Time: cur.Start.UTC().Truncate(time.Minute)},
|
|
TimeEnd: &common.SDKTime{Time: cur.End.UTC().Truncate(time.Minute)},
|
|
SearchQuery: common.String(auditSearchQuery(tenancyOCID, cur.Q)),
|
|
},
|
|
Limit: common.Int(auditSearchPageLimit),
|
|
}
|
|
if cur.Page != "" {
|
|
req.Page = &cur.Page
|
|
}
|
|
resp, err := sc.SearchLogs(ctx, req)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("search audit logs: %w", err)
|
|
}
|
|
items := make([]AuditEvent, 0, len(resp.Results))
|
|
for _, r := range resp.Results {
|
|
if ev, ok := toSearchAuditEvent(r); ok {
|
|
items = append(items, ev)
|
|
}
|
|
}
|
|
return 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 保留有展示价值的事件:SummarizeMetricsData 已在检索语句里
|
|
// 先滤(此处兜底),内网地址发起的服务互调用 CIDR 判断(查询语言不便表达);
|
|
// 无 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
|
|
}
|
|
|
|
// searchAuditContent 是 _Audit 日志 logContent 的字段投影,只取列表展示所需;
|
|
// identity / request / response 可能为 null,零值即缺省。
|
|
type searchAuditContent struct {
|
|
ID string `json:"id"`
|
|
Time *time.Time `json:"time"`
|
|
Source string `json:"source"`
|
|
Data struct {
|
|
EventName string `json:"eventName"`
|
|
ResourceName string `json:"resourceName"`
|
|
CompartmentName string `json:"compartmentName"`
|
|
Identity struct {
|
|
PrincipalName string `json:"principalName"`
|
|
IPAddress string `json:"ipAddress"`
|
|
} `json:"identity"`
|
|
Request struct {
|
|
Action string `json:"action"`
|
|
Path string `json:"path"`
|
|
} `json:"request"`
|
|
Response struct {
|
|
Status string `json:"status"`
|
|
} `json:"response"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
// toSearchAuditEvent 把日志搜索结果压平为列表 DTO;Raw 即 logContent 原文。
|
|
// 结构不符的条目丢弃(返回 false),不因单条脏数据整页失败。
|
|
func toSearchAuditEvent(r loggingsearch.SearchResult) (AuditEvent, bool) {
|
|
if r.Data == nil {
|
|
return AuditEvent{}, false
|
|
}
|
|
b, err := json.Marshal(r.Data)
|
|
if err != nil {
|
|
return AuditEvent{}, false
|
|
}
|
|
var hit struct {
|
|
LogContent json.RawMessage `json:"logContent"`
|
|
}
|
|
if err := json.Unmarshal(b, &hit); err != nil || len(hit.LogContent) == 0 {
|
|
return AuditEvent{}, false
|
|
}
|
|
var content searchAuditContent
|
|
if err := json.Unmarshal(hit.LogContent, &content); err != nil {
|
|
return AuditEvent{}, false
|
|
}
|
|
ev := searchContentToEvent(content)
|
|
ev.Raw = hit.LogContent
|
|
return ev, true
|
|
}
|
|
|
|
// searchContentToEvent 把投影字段填入列表 DTO;Raw 由调用方设置。
|
|
func searchContentToEvent(c searchAuditContent) AuditEvent {
|
|
return AuditEvent{
|
|
EventId: c.ID,
|
|
EventTime: c.Time,
|
|
EventName: c.Data.EventName,
|
|
Source: c.Source,
|
|
ResourceName: c.Data.ResourceName,
|
|
CompartmentName: c.Data.CompartmentName,
|
|
PrincipalName: c.Data.Identity.PrincipalName,
|
|
IPAddress: c.Data.Identity.IPAddress,
|
|
Status: c.Data.Response.Status,
|
|
RequestAction: c.Data.Request.Action,
|
|
RequestPath: c.Data.Request.Path,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
})
|
|
}
|