Skip to content

Go audit query client

go get github.com/truvity/sluis/audit/[email protected]

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 (
    "connectrpc.com/connect/v2"
    "connectrpc.com/connect/v2/connecthttp"

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

client := auditv1connect.NewQueryServiceClient(connect.NewClient(
    connecthttp.NewTransport(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, query)

Records in page.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, query)
    if err != nil {
        return err
    }
    // ... use page.GetItems() ...
    next := page.GetCursors().GetNext()
    if len(page.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, &auditv1.GetRequest{Profile: "security", Id: id})
p := got.GetProvenance()   // p.GetObjectKey(), p.GetLine(), p.GetDigestId(), p.GetVerifiedAt(), p.GetSealUnknown()

Provenance names the archived copy and whether a seal vouches for it:

getter value
GetDigestId the key of the seal covering the record's hour; empty until sealed
GetVerifiedAt empty until a verifier sets it
GetSealUnknown true when the service cannot tell whether the hour is sealed

The service records every read, and a refused one as a failure.