Collect Zendesk CRM logs

Supported in:

This document explains how to ingest Zendesk Customer Relationship Management (CRM) logs to Google Security Operations using Amazon S3.

Before you begin

Make sure you have the following prerequisites:

  • A Google SecOps instance.
  • Privileged access to Zendesk.
  • Privileged access to AWS(S3, Identity and Access Management (IAM), Lambda, EventBridge).

Get Zendesk prerequisites

  1. Confirm plan & role
    1. You must be a Zendesk Adminto create API tokens / OAuth clients. The Audit Logs APIis available only on Enterprise plan. (If your account isn't Enterprise, skip audit_logs in RESOURCES .)
  2. Turn on API token access (one-time)
    1. In the Admin Center, go to Apps and integrations > APIs > API configuration.
    2. Enable Allow API token access.
  3. Generate an API token (for Basic auth)
    1. Go to Apps and integrations > APIs > API tokens.
    2. Click Add API token > (optionally) add Description > Save.
    3. Copy and save the API tokennow (you won't be able to view it again).
    4. Save the admin emailthat will authenticate with this token.
      • Basic-auth format used by the Lambda: email_address/token:api_token
  4. (Optional) Create an OAuth client (for Bearer auth instead of API token)
    1. Go to Apps and integrations > APIs > OAuth clients > Add OAuth client.
    2. Fill in the Name, Unique Identifier(auto), Redirect URLs(can be placeholder if you only mint tokens with API), and Save.
    3. Create an access token for the integration and grant the minimum scopesrequired by this guide:
      • tickets:read (for Incremental Tickets)
      • auditlogs:read (for Audit Logs; Enterprise only)
      • If unsure, read also works for read-only access.
    4. Copy the access token(paste into ZENDESK_BEARER_TOKEN ) and record the client ID/secretsecurely (for future token refresh flows).
  5. Record your Zendesk base URL

    • Use https://<your_subdomain>.zendesk.com (paste into ZENDESK_BASE_URL env var).

    What to copy & save for later

    • Base URL(for example, https://acme.zendesk.com )
    • Email Addressof the administrator user (for API token auth)
    • API Token(if using AUTH_MODE=token )
    • or OAuth access token(if using AUTH_MODE=bearer )
    • (Optional): OAuth client id/secretfor lifecycle management

Configure AWS S3 bucket and IAM for Google SecOps

  1. Create Amazon S3 bucketfollowing this user guide: Creating a bucket
  2. Save bucket Nameand Regionfor future reference (for example, zendesk-crm-logs ).
  3. Create a Userfollowing this user guide: Creating an IAM user .
  4. Select the created User.
  5. Select the Security credentialstab.
  6. Click Create Access Keyin section Access Keys.
  7. Select Third-party serviceas Use case.
  8. Click Next.
  9. Optional: Add a description tag.
  10. Click Create access key.
  11. Click Download .CSV fileto save the Access Keyand Secret Access Keyfor future reference.
  12. Click Done.
  13. Select Permissionstab.
  14. Click Add permissionsin section Permissions policies.
  15. Select Add permissions.
  16. Select Attach policies directly.
  17. Search for AmazonS3FullAccesspolicy.
  18. Select the policy.
  19. Click Next.
  20. Click Add permissions.

Configure the IAM policy and role for S3 uploads

  1. In the AWS console, go to IAM > Policies.
  2. Click Create policy > JSON tab.
  3. Copy and paste the following policy.
  4. Policy JSON(replace zendesk-crm-logs if you entered a different bucket name):

      { 
      
     "Version" 
     : 
      
     "2012-10-17" 
     , 
      
     "Statement" 
     : 
      
     [ 
      
     { 
      
     "Sid" 
     : 
      
     "AllowPutObjects" 
     , 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     "s3:PutObject" 
     , 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::zendesk-crm-logs/*" 
      
     }, 
      
     { 
      
     "Sid" 
     : 
      
     "AllowGetStateObject" 
     , 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     "s3:GetObject" 
     , 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::zendesk-crm-logs/zendesk/crm/state.json" 
      
     } 
      
     ] 
     } 
     
    
  5. Click Next > Create policy.

  6. Go to IAM > Roles > Create role > AWS service > Lambda.

  7. Attach the newly created policy.

  8. Name the role ZendeskCRMToS3Role and click Create role.

Create the Lambda function

  1. In the AWS Console, go to Lambda > Functions > Create function.
  2. Click Author from scratch.
  3. Provide the following configuration details:

    Setting Value
    Name zendesk_crm_to_s3
    Runtime Python 3.13
    Architecture x86_64
    Execution role ZendeskCRMToS3Role
  4. After the function is created, open the Codetab, delete the stub and paste the following code ( zendesk_crm_to_s3.py ).

      #!/usr/bin/env python3 
     import 
      
     os 
     , 
      
     json 
     , 
      
     time 
     , 
      
     base64 
     from 
      
     urllib.request 
      
     import 
     Request 
     , 
     urlopen 
     from 
      
     urllib.error 
      
     import 
     HTTPError 
     , 
     URLError 
     import 
      
     boto3 
     S3_BUCKET 
     = 
     os 
     . 
     environ 
     [ 
     "S3_BUCKET" 
     ] 
     S3_PREFIX 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "S3_PREFIX" 
     , 
     "zendesk/crm/" 
     ) 
     STATE_KEY 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "STATE_KEY" 
     , 
     "zendesk/crm/state.json" 
     ) 
     BASE_URL 
     = 
     os 
     . 
     environ 
     [ 
     "ZENDESK_BASE_URL" 
     ] 
     . 
     rstrip 
     ( 
     "/" 
     ) 
     # e.g. https://your_subdomain.zendesk.com 
     AUTH_MODE 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "AUTH_MODE" 
     , 
     "token" 
     ) 
     . 
     lower 
     () 
     # token|bearer 
     EMAIL 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "ZENDESK_EMAIL" 
     , 
     "" 
     ) 
     API_TOKEN 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "ZENDESK_API_TOKEN" 
     , 
     "" 
     ) 
     BEARER 
     = 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "ZENDESK_BEARER_TOKEN" 
     , 
     "" 
     ) 
     RESOURCES 
     = 
     [ 
     r 
     . 
     strip 
     () 
     for 
     r 
     in 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "RESOURCES" 
     , 
     "audit_logs,incremental_tickets" 
     ) 
     . 
     split 
     ( 
     "," 
     ) 
     if 
     r 
     . 
     strip 
     ()] 
     MAX_PAGES 
     = 
     int 
     ( 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "MAX_PAGES" 
     , 
     "20" 
     )) 
     LOOKBACK 
     = 
     int 
     ( 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "LOOKBACK_SECONDS" 
     , 
     "3600" 
     )) 
     # 1h default 
     HTTP_TIMEOUT 
     = 
     int 
     ( 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "HTTP_TIMEOUT" 
     , 
     "60" 
     )) 
     HTTP_RETRIES 
     = 
     int 
     ( 
     os 
     . 
     environ 
     . 
     get 
     ( 
     "HTTP_RETRIES" 
     , 
     "3" 
     )) 
     s3 
     = 
     boto3 
     . 
     client 
     ( 
     "s3" 
     ) 
     def 
      
     _headers 
     () 
     - 
    > dict 
     : 
     if 
     AUTH_MODE 
     == 
     "bearer" 
     and 
     BEARER 
     : 
     return 
     { 
     "Authorization" 
     : 
     f 
     "Bearer 
     { 
     BEARER 
     } 
     " 
     , 
     "Accept" 
     : 
     "application/json" 
     } 
     if 
     AUTH_MODE 
     == 
     "token" 
     and 
     EMAIL 
     and 
     API_TOKEN 
     : 
     token 
     = 
     base64 
     . 
     b64encode 
     ( 
     f 
     " 
     { 
     EMAIL 
     } 
     /token: 
     { 
     API_TOKEN 
     } 
     " 
     . 
     encode 
     ()) 
     . 
     decode 
     () 
     return 
     { 
     "Authorization" 
     : 
     f 
     "Basic 
     { 
     token 
     } 
     " 
     , 
     "Accept" 
     : 
     "application/json" 
     } 
     raise 
     RuntimeError 
     ( 
     "Invalid auth settings: provide token (EMAIL + API_TOKEN) or BEARER" 
     ) 
     def 
      
     _get_state 
     () 
     - 
    > dict 
     : 
     try 
     : 
     obj 
     = 
     s3 
     . 
     get_object 
     ( 
     Bucket 
     = 
     S3_BUCKET 
     , 
     Key 
     = 
     STATE_KEY 
     ) 
     b 
     = 
     obj 
     [ 
     "Body" 
     ] 
     . 
     read 
     () 
     return 
     json 
     . 
     loads 
     ( 
     b 
     ) 
     if 
     b 
     else 
     { 
     "audit_logs" 
     : 
     {}, 
     "incremental_tickets" 
     : 
     {}} 
     except 
     Exception 
     : 
     return 
     { 
     "audit_logs" 
     : 
     {}, 
     "incremental_tickets" 
     : 
     {}} 
     def 
      
     _put_state 
     ( 
     st 
     : 
     dict 
     ) 
     - 
    > None 
     : 
     s3 
     . 
     put_object 
     ( 
     Bucket 
     = 
     S3_BUCKET 
     , 
     Key 
     = 
     STATE_KEY 
     , 
     Body 
     = 
     json 
     . 
     dumps 
     ( 
     st 
     , 
     separators 
     = 
     ( 
     "," 
     , 
     ":" 
     )) 
     . 
     encode 
     ( 
     "utf-8" 
     ), 
     ContentType 
     = 
     "application/json" 
     , 
     ) 
     def 
      
     _http_get_json 
     ( 
     url 
     : 
     str 
     ) 
     - 
    > dict 
     : 
     attempt 
     = 
     0 
     while 
     True 
     : 
     try 
     : 
     req 
     = 
     Request 
     ( 
     url 
     , 
     method 
     = 
     "GET" 
     ) 
     for 
     k 
     , 
     v 
     in 
     _headers 
     () 
     . 
     items 
     (): 
     req 
     . 
     add_header 
     ( 
     k 
     , 
     v 
     ) 
     with 
     urlopen 
     ( 
     req 
     , 
     timeout 
     = 
     HTTP_TIMEOUT 
     ) 
     as 
     r 
     : 
     return 
     json 
     . 
     loads 
     ( 
     r 
     . 
     read 
     () 
     . 
     decode 
     ( 
     "utf-8" 
     )) 
     except 
     HTTPError 
     as 
     e 
     : 
     if 
     e 
     . 
     code 
     in 
     ( 
     429 
     , 
     500 
     , 
     502 
     , 
     503 
     , 
     504 
     ) 
     and 
     attempt 
    < HTTP_RETRIES 
     : 
     ra 
     = 
     1 
     + 
     attempt 
     try 
     : 
     ra 
     = 
     int 
     ( 
     e 
     . 
     headers 
     . 
     get 
     ( 
     "Retry-After" 
     , 
     ra 
     )) 
     except 
     Exception 
     : 
     pass 
     time 
     . 
     sleep 
     ( 
     max 
     ( 
     1 
     , 
     ra 
     )) 
     attempt 
     += 
     1 
     continue 
     raise 
     except 
     URLError 
     : 
     if 
     attempt 
    < HTTP_RETRIES 
     : 
     time 
     . 
     sleep 
     ( 
     1 
     + 
     attempt 
     ) 
     attempt 
     += 
     1 
     continue 
     raise 
     def 
      
     _put_page 
     ( 
     payload 
     : 
     dict 
     , 
     resource 
     : 
     str 
     ) 
     - 
    > str 
     : 
     ts 
     = 
     time 
     . 
     gmtime 
     () 
     key 
     = 
     f 
     " 
     { 
     S3_PREFIX 
     } 
     / 
     { 
     time 
     . 
     strftime 
     ( 
     '%Y/%m/ 
     %d 
     /%H%M%S' 
     , 
      
     ts 
     ) 
     } 
     -zendesk- 
     { 
     resource 
     } 
     .json" 
     s3 
     . 
     put_object 
     ( 
     Bucket 
     = 
     S3_BUCKET 
     , 
     Key 
     = 
     key 
     , 
     Body 
     = 
     json 
     . 
     dumps 
     ( 
     payload 
     , 
     separators 
     = 
     ( 
     "," 
     , 
     ":" 
     )) 
     . 
     encode 
     ( 
     "utf-8" 
     ), 
     ContentType 
     = 
     "application/json" 
     , 
     ) 
     return 
     key 
     def 
      
     fetch_audit_logs 
     ( 
     state 
     : 
     dict 
     ): 
      
     """GET /api/v2/audit_logs.json with pagination via `next_page` (Zendesk).""" 
     next_url 
     = 
     state 
     . 
     get 
     ( 
     "next_url" 
     ) 
     or 
     f 
     " 
     { 
     BASE_URL 
     } 
     /api/v2/audit_logs.json?page=1" 
     pages 
     = 
     0 
     written 
     = 
     0 
     last_next 
     = 
     None 
     while 
     pages 
    < MAX_PAGES 
     and 
     next_url 
     : 
     data 
     = 
     _http_get_json 
     ( 
     next_url 
     ) 
     _put_page 
     ( 
     data 
     , 
     "audit_logs" 
     ) 
     written 
     += 
     len 
     ( 
     data 
     . 
     get 
     ( 
     "audit_logs" 
     , 
     [])) 
     last_next 
     = 
     data 
     . 
     get 
     ( 
     "next_page" 
     ) 
     next_url 
     = 
     last_next 
     pages 
     += 
     1 
     return 
     { 
     "resource" 
     : 
     "audit_logs" 
     , 
     "pages" 
     : 
     pages 
     , 
     "written" 
     : 
     written 
     , 
     "next_url" 
     : 
     last_next 
     } 
     def 
      
     fetch_incremental_tickets 
     ( 
     state 
     : 
     dict 
     ): 
      
     """Cursor-based incremental export: /api/v2/incremental/tickets/cursor.json (pagination via `links.next`).""" 
     next_link 
     = 
     state 
     . 
     get 
     ( 
     "next" 
     ) 
     if 
     not 
     next_link 
     : 
     start 
     = 
     int 
     ( 
     time 
     . 
     time 
     ()) 
     - 
     LOOKBACK 
     next_link 
     = 
     f 
     " 
     { 
     BASE_URL 
     } 
     /api/v2/incremental/tickets/cursor.json?start_time= 
     { 
     start 
     } 
     " 
     pages 
     = 
     0 
     written 
     = 
     0 
     last_next 
     = 
     None 
     while 
     pages 
    < MAX_PAGES 
     and 
     next_link 
     : 
     data 
     = 
     _http_get_json 
     ( 
     next_link 
     ) 
     _put_page 
     ( 
     data 
     , 
     "incremental_tickets" 
     ) 
     written 
     += 
     len 
     ( 
     data 
     . 
     get 
     ( 
     "tickets" 
     , 
     [])) 
     links 
     = 
     data 
     . 
     get 
     ( 
     "links" 
     ) 
     or 
     {} 
     next_link 
     = 
     links 
     . 
     get 
     ( 
     "next" 
     ) 
     last_next 
     = 
     next_link 
     pages 
     += 
     1 
     return 
     { 
     "resource" 
     : 
     "incremental_tickets" 
     , 
     "pages" 
     : 
     pages 
     , 
     "written" 
     : 
     written 
     , 
     "next" 
     : 
     last_next 
     } 
     def 
      
     lambda_handler 
     ( 
     event 
     = 
     None 
     , 
     context 
     = 
     None 
     ): 
     state 
     = 
     _get_state 
     () 
     summary 
     = 
     [] 
     if 
     "audit_logs" 
     in 
     RESOURCES 
     : 
     res 
     = 
     fetch_audit_logs 
     ( 
     state 
     . 
     get 
     ( 
     "audit_logs" 
     , 
     {})) 
     state 
     [ 
     "audit_logs" 
     ] 
     = 
     { 
     "next_url" 
     : 
     res 
     . 
     get 
     ( 
     "next_url" 
     )} 
     summary 
     . 
     append 
     ( 
     res 
     ) 
     if 
     "incremental_tickets" 
     in 
     RESOURCES 
     : 
     res 
     = 
     fetch_incremental_tickets 
     ( 
     state 
     . 
     get 
     ( 
     "incremental_tickets" 
     , 
     {})) 
     state 
     [ 
     "incremental_tickets" 
     ] 
     = 
     { 
     "next" 
     : 
     res 
     . 
     get 
     ( 
     "next" 
     )} 
     summary 
     . 
     append 
     ( 
     res 
     ) 
     _put_state 
     ( 
     state 
     ) 
     return 
     { 
     "ok" 
     : 
     True 
     , 
     "summary" 
     : 
     summary 
     } 
     if 
     __name__ 
     == 
     "__main__" 
     : 
     print 
     ( 
     lambda_handler 
     ()) 
     
    
  5. Go to Configuration > Environment variables.

  6. Click Edit > Add new environment variable.

  7. Enter the environment variables provided in the following table, replacing the example values with your values.

    Environment variables

    Key Example value
    S3_BUCKET zendesk-crm-logs
    S3_PREFIX zendesk/crm/
    STATE_KEY zendesk/crm/state.json
    ZENDESK_BASE_URL https://your_subdomain.zendesk.com
    AUTH_MODE token
    ZENDESK_EMAIL analyst@example.com
    ZENDESK_API_TOKEN <api_token>
    ZENDESK_BEARER_TOKEN <leave empty unless using OAuth bearer>
    RESOURCES audit_logs,incremental_tickets
    MAX_PAGES 20
    LOOKBACK_SECONDS 3600
    HTTP_TIMEOUT 60
  8. After the function is created, stay on its page (or open Lambda > Functions > your-function).

  9. Select the Configurationtab.

  10. In the General configurationpanel, click Edit.

  11. Change Timeoutto 5 minutes (300 seconds)and click Save.

Create an EventBridge schedule

  1. Go to Amazon EventBridge > Scheduler > Create schedule.
  2. Provide the following configuration details:
    • Recurring schedule: Rate( 1 hour ).
    • Target: Your Lambda function zendesk_crm_to_s3 .
    • Name: zendesk_crm_to_s3-1h .
  3. Click Create schedule.

(Optional) Create read-only IAM user & keys for Google SecOps

  1. Go to AWS Console > IAM > Users > Add users.
  2. Click Add users.
  3. Provide the following configuration details:
    • User: Enter secops-reader .
    • Access type: Select Access key – Programmatic access.
  4. Click Create user.
  5. Attach minimal read policy (custom): Users > secops-reader > Permissions > Add permissions > Attach policies directly > Create policy.
  6. JSON:

      { 
      
     "Version" 
     : 
      
     "2012-10-17" 
     , 
      
     "Statement" 
     : 
      
     [ 
      
     { 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     [ 
     "s3:GetObject" 
     ], 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::zendesk-crm-logs/*" 
      
     }, 
      
     { 
      
     "Effect" 
     : 
      
     "Allow" 
     , 
      
     "Action" 
     : 
      
     [ 
     "s3:ListBucket" 
     ], 
      
     "Resource" 
     : 
      
     "arn:aws:s3:::zendesk-crm-logs" 
      
     } 
      
     ] 
     } 
     
    
  7. Name = secops-reader-policy .

  8. Click Create policy > search/select > Next > Add permissions.

  9. Create access key for secops-reader : Security credentials > Access keys.

  10. Click Create access key.

  11. Download the .CSV . (You'll paste these values into the feed).

Configure a feed in Google SecOps to ingest Zendesk CRM logs

  1. Go to SIEM Settings > Feeds.
  2. Click + Add New Feed.
  3. In the Feed namefield, enter a name for the feed (for example, Zendesk CRM logs ).
  4. Select Amazon S3 V2as the Source type.
  5. Select Zendesk CRMas the Log type.
  6. Click Next.
  7. Specify values for the following input parameters:
    • S3 URI: s3://zendesk-crm-logs/zendesk/crm/
    • Source deletion options: Select deletion option according to your preference.
    • Maximum File Age: Include files modified in the last number of days. Default is 180 days.
    • Access Key ID: User access key with access to the S3 bucket.
    • Secret Access Key: User secret key with access to the S3 bucket.
    • Asset namespace: The asset namespace .
    • Ingestion labels: The label applied to the events from this feed.
  8. Click Next.
  9. Review your new feed configuration in the Finalizescreen, and then click Submit.

Need more help? Get answers from Community members and Google SecOps professionals.

Create a Mobile Website
View Site in Mobile | Classic
Share by: