Skip to content

Go audit query client

go get github.com/truvity/sluis/audit/sdk@vX.Y.Z

auditv1connect.QueryServiceClient reads audit.v1.QueryService. It ships in the same module as the emitter. This page follows examples/read. The API is on pkg.go.dev. Access rules are in read the trail and the wire contract in the API reference.

Make a client

import (
    auditv1 "github.com/truvity/sluis/audit/sdk/gen/audit/v1"
    "github.com/truvity/sluis/audit/sdk/gen/audit/v1/auditv1connect"
)

client := auditv1connect.NewQueryServiceClient(httpClient, "https://audit-query.example.com")

Authentication is yours. Wrap the transport and set Authorization: Bearer <token> on every request. A program gets the token by exchange. The service ANDs the caller's grants into every query.

type bearer struct{ token string }

func (b bearer) RoundTrip(r *http.Request) (*http.Response, error) {
    r = r.Clone(r.Context())
    r.Header.Set("Authorization", "Bearer "+b.token)
    return http.DefaultTransport.RoundTrip(r)
}

A filtered query

A SearchRequest names a profile and a filter. The filter is an OR-joined list of conjunctions of typed predicates. There is no free text. This one finds failed and denied actions of the last day under one prefix:

query := &auditv1.SearchRequest{
    Profile: "security",
    Filter: []*auditv1.Filter{{
        OccurredAt: &auditv1.TimePredicate{Operator: &auditv1.TimePredicate_Between{Between: &auditv1.TimeRange{
            From: timestamppb.New(time.Now().Add(-24 * time.Hour)),
            To:   timestamppb.Now(),
        }}},
        Action:  &auditv1.StringPredicate{Operator: &auditv1.StringPredicate_Prefix{Prefix: "shop.order."}},
        Outcome: &auditv1.StringPredicate{Operator: &auditv1.StringPredicate_In{In: &auditv1.StringList{Values: []string{"failure", "denied"}}}},
    }},
    Limit: 100,
}
page, err := client.Search(ctx, connect.NewRequest(query))

Records in page.Msg.GetItems() have typed getters such as GetOccurredAt() and GetActor().GetId().

Paging

Set the page cursor on the next request until a page comes back short or without one:

for {
    page, err := client.Search(ctx, connect.NewRequest(query))
    if err != nil {
        return err
    }
    // ... use page.Msg.GetItems() ...
    next := page.Msg.GetCursors().GetNext()
    if len(page.Msg.GetItems()) < int(query.GetLimit()) || next == "" {
        break
    }
    query.Cursor = next
}

Keep the last next cursor. Asked later, it returns what was recorded since.

One record and where it came from

got, err := client.Get(ctx, connect.NewRequest(&auditv1.GetRequest{Profile: "security", Id: id}))
p := got.Msg.GetProvenance()   // p.GetObjectKey(), p.GetLine(), p.GetDigestId(), p.GetVerifiedAt()

Provenance names the archived copy and whether a seal vouches for it. The digest and time stay empty until seals exist. The service records every Search and Get.