-
Notifications
You must be signed in to change notification settings - Fork 93
Add example CLI for audit log data retrieval #686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
93e9f84
Add first version of Auditlog CLI tool
tobiscr cf789a7
Support time parameters
tobiscr 30ceb7e
Simplify service binding JSON
tobiscr 59bd772
Add Readme
tobiscr 6c35ba2
Update retrieve-auditlog-data/README.md
tobiscr 1fbdf8d
Update retrieve-auditlog-data/README.md
tobiscr 71f2067
Update retrieve-auditlog-data/README.md
tobiscr d084552
Update retrieve-auditlog-data/README.md
tobiscr f6cfe9f
Update retrieve-auditlog-data/README.md
tobiscr a0b2097
Update retrieve-auditlog-data/README.md
tobiscr b64f44c
Update retrieve-auditlog-data/README.md
tobiscr 12bbfc5
Update retrieve-auditlog-data/README.md
tobiscr 3b4ea35
Update retrieve-auditlog-data/README.md
tobiscr afc008c
Update retrieve-auditlog-data/README.md
tobiscr 9a98c0a
Update retrieve-auditlog-data/README.md
tobiscr 803fc22
Apply suggestions from code review
tobiscr 0999608
Apply suggestion from @tobiscr
tobiscr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| servicebinding.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.