Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions retrieve-auditlog-data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
servicebinding.json
87 changes: 87 additions & 0 deletions retrieve-auditlog-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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).

## 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 [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

Place your service binding file in the project directory as `servicebinding.json`. It should look like this:

```json
{
"url": "https://<your-auditlog-service>.cfapps.<region>.hana.ondemand.com",
"tokenUrl": "https://<your-subdomain>.authentication.<region>.hana.ondemand.com",
"clientId": "<your-client-id>",
"clientSecret": "<your-client-secret>"
}
```

## Usage

### Get Logs with the 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.

You may see the following categories:

- `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.
Comment thread
tobiscr marked this conversation as resolved.

160 changes: 160 additions & 0 deletions retrieve-auditlog-data/cmd/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"

"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.
//
// 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 {
var timeFrom, timeTo string

cmd := &cobra.Command{
Use: "get",
Short: "Get audit log data",
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
}

if err := fetchAllPages(client, timeFrom, timeTo); err != nil {
fmt.Println("Error fetching audit logs:", err)
}
},
}

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

return cmd
}

// 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=<value>.
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.
// The client automatically fetches and refreshes the bearer token as needed.
func newClient() (*http.Client, error) {
cfg := &clientcredentials.Config{
ClientID: config.serviceBinding.ClientID,
ClientSecret: config.serviceBinding.ClientSecret,
TokenURL: config.serviceBinding.TokenURL + "/oauth/token",
}

tokenSource := cfg.TokenSource(context.TODO())
client := oauth2.NewClient(context.TODO(), tokenSource)

return client, nil
}
Loading
Loading