From 93e9f84a7425978130e56939cdb03555bcdc5098 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Mon, 22 Jun 2026 10:39:32 +0200 Subject: [PATCH 01/17] Add first version of Auditlog CLI tool --- retrieve-auditlog-data/.gitignore | 1 + retrieve-auditlog-data/cmd/get.go | 73 +++++++++++++++++++++++++++++ retrieve-auditlog-data/cmd/model.go | 14 ++++++ retrieve-auditlog-data/cmd/root.go | 63 +++++++++++++++++++++++++ retrieve-auditlog-data/go.mod | 13 +++++ retrieve-auditlog-data/go.sum | 12 +++++ retrieve-auditlog-data/main.go | 8 ++++ 7 files changed, 184 insertions(+) create mode 100644 retrieve-auditlog-data/.gitignore create mode 100644 retrieve-auditlog-data/cmd/get.go create mode 100644 retrieve-auditlog-data/cmd/model.go create mode 100644 retrieve-auditlog-data/cmd/root.go create mode 100644 retrieve-auditlog-data/go.mod create mode 100644 retrieve-auditlog-data/go.sum create mode 100644 retrieve-auditlog-data/main.go diff --git a/retrieve-auditlog-data/.gitignore b/retrieve-auditlog-data/.gitignore new file mode 100644 index 00000000..d60eff7c --- /dev/null +++ b/retrieve-auditlog-data/.gitignore @@ -0,0 +1 @@ +servicebinding.json \ No newline at end of file diff --git a/retrieve-auditlog-data/cmd/get.go b/retrieve-auditlog-data/cmd/get.go new file mode 100644 index 00000000..5d8ab23e --- /dev/null +++ b/retrieve-auditlog-data/cmd/get.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "bytes" + "context" + "io" + + "github.com/spf13/cobra" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +// newGetCmd creates the "get" subcommand that fetches and prints audit log records +// from the SAP Audit Log Service v2 API as pretty-printed JSON. +func newGetCmd() *cobra.Command { + return &cobra.Command{ + Use: "get", + Short: "Get audit log data", + Run: func(cmd *cobra.Command, args []string) { + + client, err := newClient() + if err != nil { + fmt.Println("Error creating OAuth2 client:", err) + return + } + + resp, err := client.Get(config.serviceBinding.URL + "/auditlog/v2/auditlogrecords") + if err != nil { + fmt.Println("Error making request:", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Println("Failed to fetch audit logs, status code:", resp.StatusCode) + return + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body:", err) + return + } + + var prettyJSON bytes.Buffer + if err = json.Indent(&prettyJSON, bodyBytes, "", "\t"); err != nil { + log.Println("JSON formatting failed:", err) + return + } + fmt.Println(prettyJSON.String()) + }, + } +} + +// newClient creates an OAuth2 HTTP client using client credentials from the service binding. +// The client automatically fetches and refreshes the bearer token as needed. +func newClient() (*http.Client, error) { + cfg := &clientcredentials.Config{ + ClientID: config.serviceBinding.UAA.ClientID, + ClientSecret: config.serviceBinding.UAA.ClientSecret, + TokenURL: config.serviceBinding.UAA.URL + "/oauth/token", + } + + tokenSource := cfg.TokenSource(context.TODO()) + client := oauth2.NewClient(context.TODO(), tokenSource) + + return client, nil +} diff --git a/retrieve-auditlog-data/cmd/model.go b/retrieve-auditlog-data/cmd/model.go new file mode 100644 index 00000000..ffcfabd0 --- /dev/null +++ b/retrieve-auditlog-data/cmd/model.go @@ -0,0 +1,14 @@ +package cmd + +// serviceBinding holds the connection details parsed from the SAP Audit Log Service binding file. +type serviceBinding struct { + URL string `json:"url"` + UAA uaa `json:"uaa"` +} + +// uaa holds the OAuth2 credentials for the UAA token endpoint. +type uaa struct { + URL string `json:"url"` + ClientID string `json:"clientid"` + ClientSecret string `json:"clientsecret"` +} diff --git a/retrieve-auditlog-data/cmd/root.go b/retrieve-auditlog-data/cmd/root.go new file mode 100644 index 00000000..2aa54db1 --- /dev/null +++ b/retrieve-auditlog-data/cmd/root.go @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2020 SAP SE or an SAP affiliate company and kyma-runtime-extension-samples +// SPDX-License-Identifier: Apache-2.0 + +// Package cmd implements the CLI commands for the auditlog tool. +package cmd + +import ( + "encoding/json" + "os" + + "github.com/spf13/cobra" +) + +// config holds the global CLI configuration shared across all commands. +var config = struct { + ServiceBindingFile string + serviceBinding serviceBinding +}{} + +// newRootCmd creates and returns the root cobra command. +// It registers the --bindingFile flag and loads the service binding before any subcommand runs. +func newRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "auditlog", + Short: "Access audit logs from SAP Audit Log Service v2", + Long: `This CLI tool helps you to access + audit logs from SAP Audit Log Service v2. + You can use it to query and retrieve audit logs based on various criteria.`, + } + rootCmd.PersistentFlags().StringVarP(&config.ServiceBindingFile, "bindingFile", "b", "./servicebinding.json", "Path to the service binding file") + + // Load the service binding file before executing any subcommand. + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if err := readServiceBinding(); err != nil { + return err + } + return nil + } + + rootCmd.AddCommand(newGetCmd()) + return rootCmd +} + +// Execute is the entry point called by main. It builds the command tree and runs the CLI. +func Execute() { + err := newRootCmd().Execute() + if err != nil { + os.Exit(1) + } +} + +// readServiceBinding reads and parses the JSON service binding file into the global config. +func readServiceBinding() error { + data, err := os.ReadFile(config.ServiceBindingFile) + if err != nil { + return err + } + + if err := json.Unmarshal(data, &config.serviceBinding); err != nil { + return err + } + return err +} diff --git a/retrieve-auditlog-data/go.mod b/retrieve-auditlog-data/go.mod new file mode 100644 index 00000000..4f5a4509 --- /dev/null +++ b/retrieve-auditlog-data/go.mod @@ -0,0 +1,13 @@ +module github.com/kyma/kyma-runtime-samples/retrieve-auditlog-data + +go 1.26.2 + +require ( + github.com/spf13/cobra v1.10.2 + golang.org/x/oauth2 v0.36.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/retrieve-auditlog-data/go.sum b/retrieve-auditlog-data/go.sum new file mode 100644 index 00000000..a2d200e9 --- /dev/null +++ b/retrieve-auditlog-data/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/retrieve-auditlog-data/main.go b/retrieve-auditlog-data/main.go new file mode 100644 index 00000000..a1e72998 --- /dev/null +++ b/retrieve-auditlog-data/main.go @@ -0,0 +1,8 @@ +// Package main is the entry point for the auditlog CLI tool. +package main + +import "github.com/kyma/kyma-runtime-samples/retrieve-auditlog-data/cmd" + +func main() { + cmd.Execute() +} From cf789a7984e4eb6e00f8509250754c9a09342e55 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Mon, 22 Jun 2026 11:37:36 +0200 Subject: [PATCH 02/17] Support time parameters --- retrieve-auditlog-data/cmd/get.go | 141 ++++++++++--- retrieve-auditlog-data/cmd/get_test.go | 264 +++++++++++++++++++++++++ retrieve-auditlog-data/cmd/root.go | 3 - 3 files changed, 378 insertions(+), 30 deletions(-) create mode 100644 retrieve-auditlog-data/cmd/get_test.go diff --git a/retrieve-auditlog-data/cmd/get.go b/retrieve-auditlog-data/cmd/get.go index 5d8ab23e..f244f82d 100644 --- a/retrieve-auditlog-data/cmd/get.go +++ b/retrieve-auditlog-data/cmd/get.go @@ -1,14 +1,14 @@ package cmd import ( + "bytes" + "context" "encoding/json" "fmt" + "io" "log" "net/http" - - "bytes" - "context" - "io" + "net/url" "github.com/spf13/cobra" "golang.org/x/oauth2" @@ -17,44 +17,131 @@ import ( // newGetCmd creates the "get" subcommand that fetches and prints audit log records // from the SAP Audit Log Service v2 API as pretty-printed JSON. +// +// Supported flags: +// +// --time-from Start of the time range (format: 2006-01-02T15:04:05, UTC). Defaults to 30 days back. +// --time-to End of the time range (format: 2006-01-02T15:04:05, UTC). Defaults to now. +// +// When the result set exceeds 500 records the API paginates via a Paging response header. +// All pages are fetched automatically and printed in order. func newGetCmd() *cobra.Command { - return &cobra.Command{ + var timeFrom, timeTo string + + cmd := &cobra.Command{ Use: "get", Short: "Get audit log data", - Run: func(cmd *cobra.Command, args []string) { + Long: `Retrieve audit log records from the SAP Audit Log Service v2 API. +If no time filter is provided the API returns the last 30 days of logs. +Times must be in UTC using the format: 2006-01-02T15:04:05 + +Example: + auditlog get --time-from 2024-01-01T00:00:00 --time-to 2024-01-31T23:59:59`, + Run: func(cmd *cobra.Command, args []string) { client, err := newClient() if err != nil { fmt.Println("Error creating OAuth2 client:", err) return } - resp, err := client.Get(config.serviceBinding.URL + "/auditlog/v2/auditlogrecords") - if err != nil { - fmt.Println("Error making request:", err) - return + if err := fetchAllPages(client, timeFrom, timeTo); err != nil { + fmt.Println("Error fetching audit logs:", err) } - defer resp.Body.Close() + }, + } - if resp.StatusCode != http.StatusOK { - fmt.Println("Failed to fetch audit logs, status code:", resp.StatusCode) - return - } + cmd.Flags().StringVar(&timeFrom, "time-from", "", "Start of time range in UTC (format: 2006-01-02T15:04:05)") + cmd.Flags().StringVar(&timeTo, "time-to", "", "End of time range in UTC (format: 2006-01-02T15:04:05)") - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println("Error reading response body:", err) - return - } + return cmd +} - var prettyJSON bytes.Buffer - if err = json.Indent(&prettyJSON, bodyBytes, "", "\t"); err != nil { - log.Println("JSON formatting failed:", err) - return - } - fmt.Println(prettyJSON.String()) - }, +// fetchAllPages retrieves all pages of audit log records, following the server-side +// paging handle returned in the Paging response header until no further pages exist. +func fetchAllPages(client *http.Client, timeFrom, timeTo string) error { + baseURL := config.serviceBinding.URL + "/auditlog/v2/auditlogrecords" + handle := "" + + for { + pageURL, err := buildURL(baseURL, timeFrom, timeTo, handle) + if err != nil { + return fmt.Errorf("building request URL: %w", err) + } + + resp, err := client.Get(pageURL) + if err != nil { + return fmt.Errorf("making request: %w", err) + } + + body, err := readResponse(resp) + resp.Body.Close() + if err != nil { + return err + } + + var prettyJSON bytes.Buffer + if err = json.Indent(&prettyJSON, body, "", "\t"); err != nil { + log.Println("JSON formatting failed:", err) + return err + } + fmt.Println(prettyJSON.String()) + + // The API signals more pages via the "Paging" response header: handle=. + handle = extractHandle(resp.Header.Get("Paging")) + if handle == "" { + break + } + } + return nil +} + +// buildURL constructs the request URL with optional time_from, time_to, and handle parameters. +func buildURL(baseURL, timeFrom, timeTo, handle string) (string, error) { + u, err := url.Parse(baseURL) + if err != nil { + return "", err + } + + q := u.Query() + if timeFrom != "" { + q.Set("time_from", timeFrom) + } + if timeTo != "" { + q.Set("time_to", timeTo) + } + if handle != "" { + q.Set("handle", handle) + } + u.RawQuery = q.Encode() + return u.String(), nil +} + +// extractHandle parses the handle value from the Paging response header (e.g. "handle=abc123"). +func extractHandle(pagingHeader string) string { + const prefix = "handle=" + if len(pagingHeader) > len(prefix) && pagingHeader[:len(prefix)] == prefix { + return pagingHeader[len(prefix):] + } + return "" +} + +// readResponse reads and validates the HTTP response body. +// Returns an error for non-200/204 status codes. +func readResponse(resp *http.Response) ([]byte, error) { + if resp.StatusCode == http.StatusNoContent { + return []byte("[]"), nil + } + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading response body: %w", err) } + return body, nil } // newClient creates an OAuth2 HTTP client using client credentials from the service binding. diff --git a/retrieve-auditlog-data/cmd/get_test.go b/retrieve-auditlog-data/cmd/get_test.go new file mode 100644 index 00000000..36e61168 --- /dev/null +++ b/retrieve-auditlog-data/cmd/get_test.go @@ -0,0 +1,264 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +// auditLogRecord mirrors the structure returned by the SAP Audit Log Service v2 API. +// The message field is a JSON-encoded string containing the full event detail. +type auditLogRecord struct { + MessageUUID string `json:"message_uuid"` + Time string `json:"time"` + Tenant string `json:"tenant"` + OrgID string `json:"org_id"` + SpaceID string `json:"space_id"` + AppOrServiceID string `json:"app_or_service_id"` + AlsServiceID string `json:"als_service_id"` + User string `json:"user"` + Category string `json:"category"` + FormatVersion string `json:"format_version"` + Message string `json:"message"` +} + +// auditLogMessage mirrors the structure of the JSON-encoded message field. +type auditLogMessage struct { + UUID string `json:"uuid"` + User string `json:"user"` + Time string `json:"time"` + IP string `json:"ip"` + Data string `json:"data"` + Attributes []any `json:"attributes"` + ID string `json:"id"` + Category string `json:"category"` + Tenant string `json:"tenant"` + CustomDetails map[string]any `json:"customDetails"` +} + +// newTestRecord builds a synthetic audit log record with the given category and timestamp. +// All IDs and values are generic placeholders — no real data from test-data.json. +func newTestRecord(category, timestamp string) auditLogRecord { + msg := auditLogMessage{ + UUID: "00000000-0000-0000-0000-000000000001", + User: "test-user", + Time: timestamp, + IP: "192.0.2.1", + Data: `{"level":"INFO","message":"test event"}`, + Attributes: []any{}, + ID: "00000000-0000-0000-0000-000000000002", + Category: category, + Tenant: "00000000-0000-0000-0000-000000000003", + CustomDetails: map[string]any{}, + } + msgBytes, _ := json.Marshal(msg) + + return auditLogRecord{ + MessageUUID: "00000000-0000-0000-0000-000000000001", + Time: timestamp, + Tenant: "00000000-0000-0000-0000-000000000003", + OrgID: "00000000-0000-0000-0000-000000000004", + SpaceID: "00000000-0000-0000-0000-000000000005", + AppOrServiceID: "00000000-0000-0000-0000-000000000006", + AlsServiceID: "00000000-0000-0000-0000-000000000007", + User: "test-user", + Category: category, + FormatVersion: "", + Message: string(msgBytes), + } +} + +func TestBuildURL_WithTimestamps(t *testing.T) { + base := "https://auditlog.example.com/auditlog/v2/auditlogrecords" + + result, err := buildURL(base, "2024-01-01T00:00:00", "2024-01-31T23:59:59", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + u, _ := url.Parse(result) + q := u.Query() + + if got := q.Get("time_from"); got != "2024-01-01T00:00:00" { + t.Errorf("time_from = %q, want %q", got, "2024-01-01T00:00:00") + } + if got := q.Get("time_to"); got != "2024-01-31T23:59:59" { + t.Errorf("time_to = %q, want %q", got, "2024-01-31T23:59:59") + } +} + +func TestBuildURL_WithoutTimestamps(t *testing.T) { + base := "https://auditlog.example.com/auditlog/v2/auditlogrecords" + + result, err := buildURL(base, "", "", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + u, _ := url.Parse(result) + q := u.Query() + + if q.Has("time_from") { + t.Error("time_from should not be present when empty") + } + if q.Has("time_to") { + t.Error("time_to should not be present when empty") + } +} + +func TestBuildURL_WithHandle(t *testing.T) { + base := "https://auditlog.example.com/auditlog/v2/auditlogrecords" + handle := "dGVzdC1oYW5kbGU=" + + result, err := buildURL(base, "2024-01-01T00:00:00", "2024-01-31T23:59:59", handle) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + u, _ := url.Parse(result) + q := u.Query() + + if got := q.Get("handle"); got != handle { + t.Errorf("handle = %q, want %q", got, handle) + } +} + +func TestExtractHandle(t *testing.T) { + tests := []struct { + header string + want string + }{ + {"handle=dGVzdA==", "dGVzdA=="}, + {"handle=", ""}, + {"", ""}, + {"something-else", ""}, + } + + for _, tt := range tests { + got := extractHandle(tt.header) + if got != tt.want { + t.Errorf("extractHandle(%q) = %q, want %q", tt.header, got, tt.want) + } + } +} + +func TestReadResponse_OK(t *testing.T) { + records := []auditLogRecord{newTestRecord("audit.security-events", "2024-01-15T10:00:00.000Z")} + body, _ := json.Marshal(records) + + rec := httptest.NewRecorder() + rec.WriteHeader(http.StatusOK) + rec.Write(body) + + got, err := readResponse(rec.Result()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var parsed []auditLogRecord + if err := json.Unmarshal(got, &parsed); err != nil { + t.Fatalf("response is not valid JSON: %v", err) + } + if len(parsed) != 1 { + t.Errorf("expected 1 record, got %d", len(parsed)) + } + if parsed[0].Category != "audit.security-events" { + t.Errorf("category = %q, want %q", parsed[0].Category, "audit.security-events") + } +} + +func TestReadResponse_NoContent(t *testing.T) { + rec := httptest.NewRecorder() + rec.WriteHeader(http.StatusNoContent) + + got, err := readResponse(rec.Result()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(got) != "[]" { + t.Errorf("body = %q, want %q", string(got), "[]") + } +} + +func TestReadResponse_ErrorStatus(t *testing.T) { + rec := httptest.NewRecorder() + rec.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(rec, "unauthorized") + + _, err := readResponse(rec.Result()) + if err == nil { + t.Fatal("expected error for 401, got nil") + } +} + +func TestFetchAllPages_WithTimestamps(t *testing.T) { + records := []auditLogRecord{ + newTestRecord("audit.security-events", "2024-01-10T08:00:00.000Z"), + newTestRecord("audit.configuration", "2024-01-15T12:30:00.000Z"), + } + body, _ := json.Marshal(records) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + + if got := q.Get("time_from"); got != "2024-01-01T00:00:00" { + http.Error(w, fmt.Sprintf("unexpected time_from: %q", got), http.StatusBadRequest) + return + } + if got := q.Get("time_to"); got != "2024-01-31T23:59:59" { + http.Error(w, fmt.Sprintf("unexpected time_to: %q", got), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(body) + })) + defer server.Close() + + config.serviceBinding.URL = server.URL + + if err := fetchAllPages(server.Client(), "2024-01-01T00:00:00", "2024-01-31T23:59:59"); err != nil { + t.Fatalf("fetchAllPages returned error: %v", err) + } +} + +func TestFetchAllPages_Pagination(t *testing.T) { + page1 := []auditLogRecord{newTestRecord("audit.security-events", "2024-01-10T08:00:00.000Z")} + page2 := []auditLogRecord{newTestRecord("audit.data-access", "2024-01-20T14:00:00.000Z")} + body1, _ := json.Marshal(page1) + body2, _ := json.Marshal(page2) + + page := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page++ + w.Header().Set("Content-Type", "application/json") + + if page == 1 { + w.Header().Set("Paging", "handle=dGVzdC1oYW5kbGU=") + w.WriteHeader(http.StatusOK) + w.Write(body1) + return + } + + if got := r.URL.Query().Get("handle"); got != "dGVzdC1oYW5kbGU=" { + http.Error(w, fmt.Sprintf("missing or wrong handle: %q", got), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write(body2) + })) + defer server.Close() + + config.serviceBinding.URL = server.URL + + if err := fetchAllPages(server.Client(), "2024-01-01T00:00:00", "2024-01-31T23:59:59"); err != nil { + t.Fatalf("fetchAllPages returned error: %v", err) + } + if page != 2 { + t.Errorf("expected 2 pages fetched, got %d", page) + } +} diff --git a/retrieve-auditlog-data/cmd/root.go b/retrieve-auditlog-data/cmd/root.go index 2aa54db1..5da7a5e2 100644 --- a/retrieve-auditlog-data/cmd/root.go +++ b/retrieve-auditlog-data/cmd/root.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2020 SAP SE or an SAP affiliate company and kyma-runtime-extension-samples -// SPDX-License-Identifier: Apache-2.0 - // Package cmd implements the CLI commands for the auditlog tool. package cmd From 30ceb7e15ab0d9ecdf8b82a468cf28b0d40f7c7f Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Mon, 22 Jun 2026 14:09:37 +0200 Subject: [PATCH 03/17] Simplify service binding JSON --- retrieve-auditlog-data/cmd/get.go | 6 +++--- retrieve-auditlog-data/cmd/get_test.go | 10 +++++----- retrieve-auditlog-data/cmd/model.go | 11 +++-------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/retrieve-auditlog-data/cmd/get.go b/retrieve-auditlog-data/cmd/get.go index f244f82d..ac5aea52 100644 --- a/retrieve-auditlog-data/cmd/get.go +++ b/retrieve-auditlog-data/cmd/get.go @@ -148,9 +148,9 @@ func readResponse(resp *http.Response) ([]byte, error) { // The client automatically fetches and refreshes the bearer token as needed. func newClient() (*http.Client, error) { cfg := &clientcredentials.Config{ - ClientID: config.serviceBinding.UAA.ClientID, - ClientSecret: config.serviceBinding.UAA.ClientSecret, - TokenURL: config.serviceBinding.UAA.URL + "/oauth/token", + ClientID: config.serviceBinding.ClientID, + ClientSecret: config.serviceBinding.ClientSecret, + TokenURL: config.serviceBinding.TokenURL + "/oauth/token", } tokenSource := cfg.TokenSource(context.TODO()) diff --git a/retrieve-auditlog-data/cmd/get_test.go b/retrieve-auditlog-data/cmd/get_test.go index 36e61168..08cf514a 100644 --- a/retrieve-auditlog-data/cmd/get_test.go +++ b/retrieve-auditlog-data/cmd/get_test.go @@ -27,11 +27,11 @@ type auditLogRecord struct { // auditLogMessage mirrors the structure of the JSON-encoded message field. type auditLogMessage struct { - UUID string `json:"uuid"` - User string `json:"user"` - Time string `json:"time"` - IP string `json:"ip"` - Data string `json:"data"` + UUID string `json:"uuid"` + User string `json:"user"` + Time string `json:"time"` + IP string `json:"ip"` + Data string `json:"data"` Attributes []any `json:"attributes"` ID string `json:"id"` Category string `json:"category"` diff --git a/retrieve-auditlog-data/cmd/model.go b/retrieve-auditlog-data/cmd/model.go index ffcfabd0..e3683e96 100644 --- a/retrieve-auditlog-data/cmd/model.go +++ b/retrieve-auditlog-data/cmd/model.go @@ -2,13 +2,8 @@ package cmd // serviceBinding holds the connection details parsed from the SAP Audit Log Service binding file. type serviceBinding struct { - URL string `json:"url"` - UAA uaa `json:"uaa"` -} - -// uaa holds the OAuth2 credentials for the UAA token endpoint. -type uaa struct { URL string `json:"url"` - ClientID string `json:"clientid"` - ClientSecret string `json:"clientsecret"` + TokenURL string `json:"tokenUrl"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` } From 59bd772fb3de750bb54dd3e5ba31fa6d4261ddb5 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Mon, 22 Jun 2026 14:13:33 +0200 Subject: [PATCH 04/17] Add Readme --- retrieve-auditlog-data/README.md | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 retrieve-auditlog-data/README.md diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md new file mode 100644 index 00000000..171fdae8 --- /dev/null +++ b/retrieve-auditlog-data/README.md @@ -0,0 +1,86 @@ +# Audit Log CLI + +A command-line tool to retrieve audit logs from the [SAP Audit Log Service](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-usage-for-subaccounts-in-cloud-foundry-environment) on SAP BTP. + +## What it does + +The tool connects to your SAP BTP Audit Log Service instance and downloads audit log records as JSON. You can filter by time range to narrow down what you retrieve. If the result is large, all pages are fetched and printed automatically. + +## Prerequisites + +- [Go 1.21+](https://go.dev/dl/) installed +- A service binding file for the `auditlog-management` service instance (see [SAP BTP documentation](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-for-global-accounts-in-cloud-foundry-environment)) + +## Setup + +Place your service binding file in the project directory as `servicebinding.json`. It should look like this: + +```json +{ + "url": "https://.cfapps..hana.ondemand.com", + "tokenUrl": "https://.authentication..hana.ondemand.com", + "clientId": "", + "clientSecret": "" +} +``` + +## Usage + +### Get logs (service default time range) + +```bash +go run main.go get +``` + +### Get logs for a specific time range + +All times are in UTC, format `YYYY-MM-DDTHH:MM:SS`. + +```bash +go run main.go get --time-from 2026-06-01T00:00:00 --time-to 2026-06-22T23:59:59 +``` + +### Get logs for the last 15 minutes + +```bash +go run main.go get \ + --time-from $(date -u -v-15M '+%Y-%m-%dT%H:%M:%S') \ + --time-to $(date -u '+%Y-%m-%dT%H:%M:%S') +``` + +### Use a different binding file + +```bash +go run main.go get --bindingFile /path/to/my-binding.json +``` + +### Save output to a file + +```bash +go run main.go get --time-from 2026-06-01T00:00:00 --time-to 2026-06-22T23:59:59 > logs.json +``` + +## Flags + +| Flag | Description | Default | +|---|---|---| +| `--time-from` | Start of the time range (UTC) | Service default | +| `--time-to` | End of the time range (UTC) | Service default | +| `--bindingFile` / `-b` | Path to the service binding file | `./servicebinding.json` | + +## Output + +Records are printed as pretty-printed JSON, one page at a time. Each record contains fields such as `message_uuid`, `time`, `category`, `user`, and a `message` field with the full event detail. + +Categories you may see: + +- `audit.security-events` — login attempts, token issuance +- `audit.configuration` — configuration changes +- `audit.data-access` — data read operations +- `audit.data-modification` — data write or delete operations + +## Notes + +- The service returns up to 500 records per page. The tool follows pagination automatically so you always get the full result. +- Logs are not immediately available after an event occurs — there may be a short delay before they appear. +- The API rate limit is 4–8 requests per second depending on your region. For large time ranges this is handled transparently. From 6c35ba294d0760634ceca58a8b354c25615725c1 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:13:14 +0200 Subject: [PATCH 05/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 171fdae8..816abe90 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -1,6 +1,6 @@ # Audit Log CLI -A command-line tool to retrieve audit logs from the [SAP Audit Log Service](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-usage-for-subaccounts-in-cloud-foundry-environment) on SAP BTP. +Audit log CLI is a command-line tool to retrieve audit logs from the [SAP Audit Log Service](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-usage-for-subaccounts-in-cloud-foundry-environment) on SAP Business Technology Platform (BTP). ## What it does From 1fbdf8d315689299671b24f4c2550dbff6886db2 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:13:23 +0200 Subject: [PATCH 06/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 816abe90..0f8305b4 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -2,7 +2,7 @@ Audit log CLI is a command-line tool to retrieve audit logs from the [SAP Audit Log Service](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-usage-for-subaccounts-in-cloud-foundry-environment) on SAP Business Technology Platform (BTP). -## What it does +## What It Does The tool connects to your SAP BTP Audit Log Service instance and downloads audit log records as JSON. You can filter by time range to narrow down what you retrieve. If the result is large, all pages are fetched and printed automatically. From 71f2067eb2d3b2ec10cc82357133d2fc83c02afa Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:13:35 +0200 Subject: [PATCH 07/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 0f8305b4..86f9f542 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -9,7 +9,7 @@ The tool connects to your SAP BTP Audit Log Service instance and downloads audit ## Prerequisites - [Go 1.21+](https://go.dev/dl/) installed -- A service binding file for the `auditlog-management` service instance (see [SAP BTP documentation](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-for-global-accounts-in-cloud-foundry-environment)) +- A service binding file for the `auditlog-management` service instance (see [Audit Log Retrieval API for Global Accounts in the Cloud Foundry Environment](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-for-global-accounts-in-cloud-foundry-environment)) ## Setup From d0845526d2b63b50bbff85de3be9798ee9a91eea Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:13:45 +0200 Subject: [PATCH 08/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 86f9f542..32ec0596 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -26,7 +26,7 @@ Place your service binding file in the project directory as `servicebinding.json ## Usage -### Get logs (service default time range) +### Get Logs with the Service Default Time Range ```bash go run main.go get From f6cfe9f8eb6cbda2d1d334e9b91b7c786d019c82 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:13:51 +0200 Subject: [PATCH 09/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 32ec0596..5aeee5fb 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -32,7 +32,7 @@ Place your service binding file in the project directory as `servicebinding.json go run main.go get ``` -### Get logs for a specific time range +### Get Logs for a Specific Time Range All times are in UTC, format `YYYY-MM-DDTHH:MM:SS`. From a0b2097c1813697631ae2ee79fc29205c47df481 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:13:58 +0200 Subject: [PATCH 10/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 5aeee5fb..e61d0b05 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -48,7 +48,7 @@ go run main.go get \ --time-to $(date -u '+%Y-%m-%dT%H:%M:%S') ``` -### Use a different binding file +### Use a Different Binding File ```bash go run main.go get --bindingFile /path/to/my-binding.json From b64f44cac4576f63bd800ff0e552c170a30ea65f Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:14:09 +0200 Subject: [PATCH 11/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index e61d0b05..15f5b49f 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -40,7 +40,7 @@ All times are in UTC, format `YYYY-MM-DDTHH:MM:SS`. go run main.go get --time-from 2026-06-01T00:00:00 --time-to 2026-06-22T23:59:59 ``` -### Get logs for the last 15 minutes +### Get Logs for the Last 15 Minutes ```bash go run main.go get \ From 12bbfc520267fddad339357295c0da3d5a961d70 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:14:18 +0200 Subject: [PATCH 12/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 15f5b49f..b950aa13 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -54,7 +54,7 @@ go run main.go get \ go run main.go get --bindingFile /path/to/my-binding.json ``` -### Save output to a file +### Save Output to a File ```bash go run main.go get --time-from 2026-06-01T00:00:00 --time-to 2026-06-22T23:59:59 > logs.json From 3b4ea35feb310f49ae989bcfc04546c93c3d1b9a Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:14:29 +0200 Subject: [PATCH 13/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index b950aa13..1aaa69dc 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -70,7 +70,7 @@ go run main.go get --time-from 2026-06-01T00:00:00 --time-to 2026-06-22T23:59:59 ## Output -Records are printed as pretty-printed JSON, one page at a time. Each record contains fields such as `message_uuid`, `time`, `category`, `user`, and a `message` field with the full event detail. +Records are printed as pretty-printed JSON, one page at a time. Each record contains fields such as **message_uuid**, **time**, **category**, **user**, and a **message** field with the full event detail. Categories you may see: From afc008c8ef046686b12e9e3dc33645b42bf24e54 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:14:40 +0200 Subject: [PATCH 14/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 1aaa69dc..0d0e574f 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -72,7 +72,7 @@ go run main.go get --time-from 2026-06-01T00:00:00 --time-to 2026-06-22T23:59:59 Records are printed as pretty-printed JSON, one page at a time. Each record contains fields such as **message_uuid**, **time**, **category**, **user**, and a **message** field with the full event detail. -Categories you may see: +You may see the following categories: - `audit.security-events` — login attempts, token issuance - `audit.configuration` — configuration changes From 9a98c0a1f7c877f00748bfaf808f311dd9e4be74 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 13:14:50 +0200 Subject: [PATCH 15/17] Update retrieve-auditlog-data/README.md Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 0d0e574f..afd4348c 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -83,4 +83,4 @@ You may see the following categories: - The service returns up to 500 records per page. The tool follows pagination automatically so you always get the full result. - Logs are not immediately available after an event occurs — there may be a short delay before they appear. -- The API rate limit is 4–8 requests per second depending on your region. For large time ranges this is handled transparently. +- The API rate limit is 4–8 requests per second, depending on your region. For large time ranges, this is handled transparently. From 803fc22b6015055bdf07dcefe87ead4ff1af3424 Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 23 Jun 2026 14:57:04 +0200 Subject: [PATCH 16/17] Apply suggestions from code review Co-authored-by: Iwona Langer --- retrieve-auditlog-data/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index afd4348c..12c37351 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -1,4 +1,4 @@ -# Audit Log CLI +# Retrieve Adit Log Data Using Audit Log CLI Audit log CLI is a command-line tool to retrieve audit logs from the [SAP Audit Log Service](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-retrieval-api-usage-for-subaccounts-in-cloud-foundry-environment) on SAP Business Technology Platform (BTP). From 09996082c8ed8026e20feb8879aa494901ba6c5c Mon Sep 17 00:00:00 2001 From: Tobias Schuhmacher Date: Tue, 30 Jun 2026 08:06:56 +0200 Subject: [PATCH 17/17] Apply suggestion from @tobiscr --- retrieve-auditlog-data/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/retrieve-auditlog-data/README.md b/retrieve-auditlog-data/README.md index 12c37351..2b278e5c 100644 --- a/retrieve-auditlog-data/README.md +++ b/retrieve-auditlog-data/README.md @@ -84,3 +84,4 @@ You may see the following categories: - The service returns up to 500 records per page. The tool follows pagination automatically so you always get the full result. - Logs are not immediately available after an event occurs — there may be a short delay before they appear. - The API rate limit is 4–8 requests per second, depending on your region. For large time ranges, this is handled transparently. +