openai--openai-cookbook
410 行
15 KiB
Plaintext
410 行
15 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# OpenAI Compliance Logs Platform quickstart\n",
|
|
"\n",
|
|
"Use this notebook to get started using the OpenAI Compliance Logs Platform. The examples focus on downloading log files so you can ingest them into your SIEM or data lake.\n",
|
|
"\n",
|
|
"- [Help Center Overview](https://help.openai.com/en/articles/9261474-compliance-api-for-chatgpt-enterprise-edu-and-chatgpt-for-teachers)\n",
|
|
"- [API Reference](https://chatgpt.com/admin/api-reference#tag/Compliance-API-Logs-Platform)\n"
|
|
],
|
|
"id": "3c9bde9be51b8b78"
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Prerequisites\n",
|
|
"\n",
|
|
"- An Enterprise Compliance API key exported as `COMPLIANCE_API_KEY`.\n",
|
|
"- The ChatGPT account ID or the API Platform Org ID for the principal in question.\n",
|
|
"- Specific requirements for your environment"
|
|
],
|
|
"id": "18f32c1126f4e3ce"
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Quickstart Scripts\n",
|
|
"\n",
|
|
"Provided below are functionally identical scripts - one for Unix-based and one for Windows-based environments.\n",
|
|
"These scripts give an example of how one could build an integration with the Compliance API to retrieve and process\n",
|
|
"log data for given event types and time ranges.\n",
|
|
"These scripts handle listing and paging through the available log files and downloading them - writing the output to stdout.\n",
|
|
"\n",
|
|
"Example invocations of these scripts are embedded in their help blocks - execute them with no arguments to see them."
|
|
],
|
|
"id": "4388a0474fcabd7"
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": [
|
|
"## Option 1: Unix-based\n",
|
|
"\n",
|
|
"Prerequisites:\n",
|
|
"- Save the script locally as `download_compliance_files.sh` and mark it executable\n",
|
|
"- Make sure you have up-to-date `bash`, `curl`, `sed`, and `date` installed.\n",
|
|
"- Format the date you want to get every log `after` as an ISO 8601 string including timezone.\n",
|
|
"\n",
|
|
"Run the script akin to `./download_compliance_files.sh <workspace_or_org_id> <event_type> <limit> <after>`"
|
|
],
|
|
"id": "e6999916958ccbba"
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": [
|
|
"```bash\n",
|
|
"#!/usr/bin/env bash\n",
|
|
"set -euo pipefail\n",
|
|
"\n",
|
|
"usage() {\n",
|
|
" echo \"Usage: $0 <workspace_or_org_id> <event_type> <limit> <after>\" >&2\n",
|
|
" echo >&2\n",
|
|
" echo 'Examples: ' >&2\n",
|
|
" echo 'COMPLIANCE_API_KEY=<KEY> ./download_compliance_files.sh f7f33107-5fb9-4ee1-8922-3eae76b5b5a0 AUTH_LOG 100 \"$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)\" > output.jsonl' >&2\n",
|
|
" echo 'COMPLIANCE_API_KEY=<KEY> ./download_compliance_files.sh org-p13k3klgno5cqxbf0q8hpgrk AUTH_LOG 100 \"$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)\" > output.jsonl' >&2\n",
|
|
"}\n",
|
|
"\n",
|
|
"if [[ $# -ne 4 ]]; then\n",
|
|
" usage\n",
|
|
" exit 2\n",
|
|
"fi\n",
|
|
"\n",
|
|
"PRINCIPAL_ID=\"$1\"\n",
|
|
"EVENT_TYPE=\"$2\"\n",
|
|
"LIMIT=\"$3\"\n",
|
|
"INITIAL_AFTER=\"$4\"\n",
|
|
"\n",
|
|
"# Require COMPLIANCE_API_KEY to be present and non-empty before using it\n",
|
|
"if [[ -z \"${COMPLIANCE_API_KEY:-}\" ]]; then\n",
|
|
" echo \"COMPLIANCE_API_KEY environment variable is required. e.g.:\" >&2\n",
|
|
" echo \"COMPLIANCE_API_KEY=<KEY> $0 <workspace_or_org_id> <event_type> <limit> <after>\" >&2\n",
|
|
" exit 2\n",
|
|
"fi\n",
|
|
"\n",
|
|
"API_BASE=\"https://api.chatgpt.com/v1/compliance\"\n",
|
|
"AUTH_HEADER=(\"-H\" \"Authorization: Bearer ${COMPLIANCE_API_KEY}\")\n",
|
|
"\n",
|
|
"# Determine whether the first arg is a workspace ID or an org ID.\n",
|
|
"# If it starts with \"org-\" treat it as an organization ID and switch the path segment accordingly.\n",
|
|
"SCOPE_SEGMENT=\"workspaces\"\n",
|
|
"if [[ \"${PRINCIPAL_ID}\" == org-* ]]; then\n",
|
|
" SCOPE_SEGMENT=\"organizations\"\n",
|
|
"fi\n",
|
|
"\n",
|
|
"# Perform a curl request and fail fast on HTTP errors, logging context to stderr.\n",
|
|
"# Usage: perform_curl \"description of action\" <curl args...>\n",
|
|
"perform_curl() {\n",
|
|
" local description=\"$1\"\n",
|
|
" shift\n",
|
|
" # Capture body and HTTP status code, keeping body on stdout-like var\n",
|
|
" # We append a newline before the status to reliably split even if body has no trailing newline.\n",
|
|
" local combined\n",
|
|
" if ! combined=$(curl -sS -w \"\\n%{http_code}\" \"$@\"); then\n",
|
|
" echo \"Network/transport error while ${description}\" >&2\n",
|
|
" exit 1\n",
|
|
" fi\n",
|
|
" local http_code\n",
|
|
" http_code=\"${combined##*$'\\n'}\"\n",
|
|
" local body\n",
|
|
" body=\"${combined%$'\\n'*}\"\n",
|
|
"\n",
|
|
" if [[ ! \"${http_code}\" =~ ^2[0-9][0-9]$ ]]; then\n",
|
|
" echo \"HTTP error ${http_code} while ${description}:\" >&2\n",
|
|
" if [[ -n \"${body}\" ]]; then\n",
|
|
" # Print the body to stderr so it doesn't corrupt stdout stream\n",
|
|
" echo \"${body}\" | jq . >&2\n",
|
|
" fi\n",
|
|
" exit 1\n",
|
|
" fi\n",
|
|
"\n",
|
|
" # On success, emit body to stdout for callers to consume\n",
|
|
" echo \"${body}\"\n",
|
|
"}\n",
|
|
"\n",
|
|
"list_logs() {\n",
|
|
" local after=\"$1\"\n",
|
|
" perform_curl \"listing logs (after=${after}, event_type=${EVENT_TYPE}, limit=${LIMIT})\" \\\n",
|
|
" -G \\\n",
|
|
" \"${API_BASE}/${SCOPE_SEGMENT}/${PRINCIPAL_ID}/logs\" \\\n",
|
|
" \"${AUTH_HEADER[@]}\" \\\n",
|
|
" --data-urlencode \"limit=${LIMIT}\" \\\n",
|
|
" --data-urlencode \"event_type=${EVENT_TYPE}\" \\\n",
|
|
" --data-urlencode \"after=${after}\"\n",
|
|
"}\n",
|
|
"\n",
|
|
"download_log() {\n",
|
|
" local id=\"$1\"\n",
|
|
" echo \"Fetching logs for ID: ${id}\" >&2\n",
|
|
" perform_curl \"downloading log id=${id}\" \\\n",
|
|
" -G -L \\\n",
|
|
" \"${API_BASE}/${SCOPE_SEGMENT}/${PRINCIPAL_ID}/logs/${id}\" \\\n",
|
|
" \"${AUTH_HEADER[@]}\"\n",
|
|
"}\n",
|
|
"\n",
|
|
"to_local_human() {\n",
|
|
" local iso=\"$1\"\n",
|
|
" if [[ -z \"${iso}\" || \"${iso}\" == \"null\" ]]; then\n",
|
|
" echo \"\"\n",
|
|
" return 0\n",
|
|
" fi\n",
|
|
"\n",
|
|
" local iso_norm\n",
|
|
" iso_norm=$(echo -n \"${iso}\" \\\n",
|
|
" | sed -E 's/\\.[0-9]+(Z|[+-][0-9:]+)$/\\1/' \\\n",
|
|
" | sed -E 's/([+-]00:00)$/Z/')\n",
|
|
"\n",
|
|
" # macOS/BSD date: parse UTC to epoch then format in local timezone\n",
|
|
" local epoch\n",
|
|
" epoch=$(date -j -u -f \"%Y-%m-%dT%H:%M:%SZ\" \"${iso_norm}\" +%s 2>/dev/null) || true\n",
|
|
" if [[ -n \"${epoch}\" ]]; then\n",
|
|
" date -r \"${epoch}\" \"+%Y-%m-%d %H:%M:%S %Z\" 2>/dev/null && return 0\n",
|
|
" fi\n",
|
|
"\n",
|
|
" # Fallback to original if parsing failed\n",
|
|
" echo \"${iso}\"\n",
|
|
"}\n",
|
|
"\n",
|
|
"current_after=\"${INITIAL_AFTER}\"\n",
|
|
"page=1\n",
|
|
"total_downloaded=0\n",
|
|
"while true; do\n",
|
|
" echo \"Fetching page ${page} with after='${current_after}' (local: $(to_local_human \"${current_after}\"))\" >&2\n",
|
|
" response_json=\"$(list_logs \"${current_after}\")\"\n",
|
|
"\n",
|
|
" # Count and download each ID from the current page (if any)\n",
|
|
" page_count=\"$(echo \"${response_json}\" | jq '.data | length')\"\n",
|
|
" if [[ \"${page_count}\" -gt 0 ]]; then\n",
|
|
" echo \"${response_json}\" | jq -r '.data[].id' | while read -r id; do\n",
|
|
" download_log \"${id}\"\n",
|
|
" done\n",
|
|
" total_downloaded=$((total_downloaded + page_count))\n",
|
|
" fi\n",
|
|
"\n",
|
|
" has_more=\"$(echo \"${response_json}\" | jq -r '.has_more')\"\n",
|
|
" current_after=\"$(echo \"${response_json}\" | jq -r '.last_end_time')\"\n",
|
|
" if [[ \"${has_more}\" == \"true\" ]]; then\n",
|
|
" page=$((page + 1))\n",
|
|
" else\n",
|
|
" break\n",
|
|
" fi\n",
|
|
"done\n",
|
|
"\n",
|
|
"if [[ \"${total_downloaded}\" -eq 0 && ( -z \"${current_after}\" || \"${current_after}\" == \"null\" ) ]]; then\n",
|
|
" echo \"No results found for event_type ${EVENT_TYPE} after ${INITIAL_AFTER}\" >&2\n",
|
|
"else\n",
|
|
" echo \"Completed downloading ${total_downloaded} log files up to ${current_after} (local: $(to_local_human \"${current_after}\"))\" >&2\n",
|
|
"fi\n",
|
|
"```"
|
|
],
|
|
"id": "e6ed22a253b67850"
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": [
|
|
"## Option 2: Windows-based\n",
|
|
"\n",
|
|
"Prerequisites:\n",
|
|
"- Save the script locally as `download_compliance_files.ps1`\n",
|
|
"- Open PowerShell (Version 5.1+) and navigate to the directory where the script is saved.\n",
|
|
"\n",
|
|
"Run the script akin to `.\\download_compliance_files.ps1 <workspace_or_org_id> <event_type> <limit> <after>`"
|
|
],
|
|
"id": "206e977bd1b7a441"
|
|
},
|
|
{
|
|
"metadata": {},
|
|
"cell_type": "markdown",
|
|
"source": [
|
|
"```ps\n",
|
|
"#!/usr/bin/env pwsh\n",
|
|
"#Requires -Version 5.1\n",
|
|
"\n",
|
|
"Set-StrictMode -Version Latest\n",
|
|
"$ErrorActionPreference = 'Stop'\n",
|
|
"\n",
|
|
"Add-Type -AssemblyName System.Web\n",
|
|
"\n",
|
|
"function Show-Usage {\n",
|
|
" [Console]::Error.WriteLine(@\"\n",
|
|
"Usage: .\\download_compliance_files.ps1 <workspace_or_org_id> <event_type> <limit> <after>\n",
|
|
"\n",
|
|
"Example:\n",
|
|
" `$env:COMPLIANCE_API_KEY = '<KEY>'\n",
|
|
" .\\download_compliance_files.ps1 f7f33107-5fb9-4ee1-8922-3eae76b5b5a0 AUTH_LOG 100 (Get-Date -AsUTC).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') |\n",
|
|
" Out-File -Encoding utf8 output.jsonl\n",
|
|
"\n",
|
|
"Example (org id):\n",
|
|
" `$env:COMPLIANCE_API_KEY = '<KEY>'\n",
|
|
" .\\download_compliance_files.ps1 org-p13k3klgno5cqxbf0q8hpgrk AUTH_LOG 100 (Get-Date -AsUTC).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') |\n",
|
|
" Out-File -Encoding utf8 output.jsonl\n",
|
|
"\"@)\n",
|
|
"}\n",
|
|
"\n",
|
|
"if ($args.Count -ne 4) {\n",
|
|
" Show-Usage\n",
|
|
" exit 2\n",
|
|
"}\n",
|
|
"\n",
|
|
"if (-not $env:COMPLIANCE_API_KEY) {\n",
|
|
" [Console]::Error.WriteLine('COMPLIANCE_API_KEY environment variable must be set.')\n",
|
|
" exit 2\n",
|
|
"}\n",
|
|
"\n",
|
|
"$PrincipalId = $args[0]\n",
|
|
"$EventType = $args[1]\n",
|
|
"$Limit = $args[2]\n",
|
|
"$InitialAfter = $args[3]\n",
|
|
"\n",
|
|
"$ApiBase = 'https://api.chatgpt.com/v1/compliance'\n",
|
|
"\n",
|
|
"if ($PrincipalId.StartsWith('org-')) {\n",
|
|
" $ScopeSegment = 'organizations'\n",
|
|
"} else {\n",
|
|
" $ScopeSegment = 'workspaces'\n",
|
|
"}\n",
|
|
"\n",
|
|
"$handler = [System.Net.Http.HttpClientHandler]::new()\n",
|
|
"$client = [System.Net.Http.HttpClient]::new($handler)\n",
|
|
"$client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $env:COMPLIANCE_API_KEY)\n",
|
|
"\n",
|
|
"function Invoke-ComplianceRequest {\n",
|
|
" param(\n",
|
|
" [Parameter(Mandatory = $true)] [string] $Description,\n",
|
|
" [Parameter(Mandatory = $true)] [string] $Path,\n",
|
|
" [hashtable] $Query = @{}\n",
|
|
" )\n",
|
|
"\n",
|
|
" $builder = [System.UriBuilder]::new(\"$ApiBase/$ScopeSegment/$PrincipalId/$Path\")\n",
|
|
" $queryString = [System.Web.HttpUtility]::ParseQueryString($builder.Query)\n",
|
|
" foreach ($key in $Query.Keys) {\n",
|
|
" $queryString[$key] = $Query[$key]\n",
|
|
" }\n",
|
|
" $builder.Query = $queryString.ToString()\n",
|
|
"\n",
|
|
" try {\n",
|
|
" $response = $client.GetAsync($builder.Uri).GetAwaiter().GetResult()\n",
|
|
" } catch {\n",
|
|
" [Console]::Error.WriteLine(\"Network/transport error while $Description\")\n",
|
|
" exit 1\n",
|
|
" }\n",
|
|
"\n",
|
|
" $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()\n",
|
|
" if (-not $response.IsSuccessStatusCode) {\n",
|
|
" [Console]::Error.WriteLine(\"HTTP error $($response.StatusCode.value__) while ${Description}:\")\n",
|
|
" if ($body) {\n",
|
|
" try {\n",
|
|
" $parsed = $body | ConvertFrom-Json\n",
|
|
" $parsed | ConvertTo-Json -Depth 10 | Write-Error\n",
|
|
" } catch {\n",
|
|
" [Console]::Error.WriteLine($body)\n",
|
|
" }\n",
|
|
" }\n",
|
|
" exit 1\n",
|
|
" }\n",
|
|
"\n",
|
|
" Write-Output $body\n",
|
|
"}\n",
|
|
"\n",
|
|
"function List-Logs {\n",
|
|
" param(\n",
|
|
" [Parameter(Mandatory = $true)] [string] $After\n",
|
|
" )\n",
|
|
"\n",
|
|
" Invoke-ComplianceRequest -Description \"listing logs (after=$After, event_type=$EventType, limit=$Limit)\" -Path 'logs' -Query @{\n",
|
|
" limit = $Limit\n",
|
|
" event_type = $EventType\n",
|
|
" after = $After\n",
|
|
" }\n",
|
|
"}\n",
|
|
"\n",
|
|
"function Download-Log {\n",
|
|
" param(\n",
|
|
" [Parameter(Mandatory = $true)] [string] $Id\n",
|
|
" )\n",
|
|
"\n",
|
|
" [Console]::Error.WriteLine(\"Fetching logs for ID: $Id\")\n",
|
|
" Invoke-ComplianceRequest -Description \"downloading log id=$Id\" -Path \"logs/$Id\"\n",
|
|
"}\n",
|
|
"\n",
|
|
"function ConvertTo-LocalHuman {\n",
|
|
" param(\n",
|
|
" [string] $Iso\n",
|
|
" )\n",
|
|
"\n",
|
|
" if (-not $Iso -or $Iso -eq 'null') {\n",
|
|
" return ''\n",
|
|
" }\n",
|
|
"\n",
|
|
" try {\n",
|
|
" $dt = [datetimeoffset]::Parse($Iso)\n",
|
|
" return $dt.ToLocalTime().ToString('yyyy-MM-dd HH:mm:ss zzz')\n",
|
|
" } catch {\n",
|
|
" return $Iso\n",
|
|
" }\n",
|
|
"}\n",
|
|
"\n",
|
|
"$currentAfter = $InitialAfter\n",
|
|
"$page = 1\n",
|
|
"$totalDownloaded = 0\n",
|
|
"while ($true) {\n",
|
|
" [Console]::Error.WriteLine(\"Fetching page $page with after='$currentAfter' (local: $(ConvertTo-LocalHuman -Iso $currentAfter))\")\n",
|
|
" $responseJson = List-Logs -After $currentAfter\n",
|
|
" $responseObj = $responseJson | ConvertFrom-Json\n",
|
|
"\n",
|
|
" $pageCount = $responseObj.data.Count\n",
|
|
" if ($pageCount -gt 0) {\n",
|
|
" foreach ($entry in $responseObj.data) {\n",
|
|
" Download-Log -Id $entry.id\n",
|
|
" }\n",
|
|
" $totalDownloaded += $pageCount\n",
|
|
" }\n",
|
|
"\n",
|
|
" $hasMore = $false\n",
|
|
" if ($null -ne $responseObj.has_more) {\n",
|
|
" $hasMore = [System.Convert]::ToBoolean($responseObj.has_more)\n",
|
|
" }\n",
|
|
"\n",
|
|
" $currentAfter = $responseObj.last_end_time\n",
|
|
" if ($hasMore) {\n",
|
|
" $page += 1\n",
|
|
" } else {\n",
|
|
" break\n",
|
|
" }\n",
|
|
"}\n",
|
|
"\n",
|
|
"if ($totalDownloaded -eq 0 -and ([string]::IsNullOrEmpty($currentAfter) -or $currentAfter -eq 'null')) {\n",
|
|
" [Console]::Error.WriteLine(\"No results found for event_type $EventType after $InitialAfter\")\n",
|
|
"} else {\n",
|
|
" [Console]::Error.WriteLine(\"Completed downloading $totalDownloaded log files up to $currentAfter (local: $(ConvertTo-LocalHuman -Iso $currentAfter))\")\n",
|
|
"}\n",
|
|
"\n",
|
|
"$client.Dispose()\n",
|
|
"$handler.Dispose()\n",
|
|
"```"
|
|
],
|
|
"id": "2a26163e0617856a"
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"name": "python",
|
|
"version": "3.11"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|