收集 Zendesk CRM 日志

支持的平台:

本文档介绍了如何使用 Amazon S3 将 Zendesk 客户关系管理 (CRM) 日志注入到 Google Security Operations。

准备工作

请确保满足以下前提条件:

  • Google SecOps 实例。
  • Zendesk 的特权访问权限。
  • AWS(S3、Identity and Access Management (IAM)、Lambda、EventBridge)的特权访问权限。

获取 Zendesk 前提条件

  1. 确认方案和角色
    1. 您必须是 Zendesk 管理员,才能创建 API 令牌 / OAuth 客户端。Audit Logs API 仅适用于企业版方案。(如果您的账号不是企业版账号,请跳过 RESOURCES 中的 audit_logs。)
  2. 开启 API 令牌访问权限(一次性)
    1. 管理中心内,依次前往应用和集成 > API > API 配置
    2. 启用允许 API 令牌访问
  3. 生成 API 令牌(用于基本身份验证)
    1. 依次前往应用和集成> API > API 令牌
    2. 依次点击添加 API 令牌>(可选)添加说明 > 保存
    3. 立即复制并保存 API 令牌(您将无法再次查看该令牌)。
    4. 保存将使用此令牌进行身份验证的管理员电子邮件地址
      • Lambda 使用的基本身份验证格式:email_address/token:api_token
  4. (可选)创建 OAuth 客户端(用于不记名身份验证,而非 API 令牌)
    1. 依次前往应用和集成> API > OAuth 客户端 > 添加 OAuth 客户端
    2. 填写名称唯一标识符(自动)、重定向网址(如果您仅使用 API 铸造令牌,则可以是占位网址),然后点击保存
    3. 为集成创建访问令牌,并授予本指南所需的最低范围
      • tickets:read(对于增量工单)
      • auditlogs:read(适用于审核日志;仅限企业版)
      • 如果不确定,read 也适用于只读访问权限。
    4. 复制访问令牌(粘贴到 ZENDESK_BEARER_TOKEN 中),并安全地记录客户端 ID/密钥(以供日后进行令牌刷新流程)。
  5. 记录您的 Zendesk 基本网址

    • 使用 https://<your_subdomain>.zendesk.com(粘贴到 ZENDESK_BASE_URL 环境变量中)。

    要复制和保存哪些内容以供日后使用

    • 基础网址(例如 https://acme.zendesk.com
    • 管理员用户的电子邮件地址(用于 API 令牌身份验证)
    • API 令牌(如果使用 AUTH_MODE=token
    • OAuth 访问令牌(如果使用 AUTH_MODE=bearer
    • (可选):用于生命周期管理的 OAuth 客户端 ID/密钥

为 Google SecOps 配置 AWS S3 存储桶和 IAM

  1. 按照以下用户指南创建 Amazon S3 存储桶创建存储桶
  2. 保存存储桶名称区域以供日后参考(例如 zendesk-crm-logs)。
  3. 按照以下用户指南创建用户创建 IAM 用户
  4. 选择创建的用户
  5. 选择安全凭据标签页。
  6. 访问密钥部分中,点击创建访问密钥
  7. 选择第三方服务作为使用情形
  8. 点击下一步
  9. 可选:添加说明标记。
  10. 点击创建访问密钥
  11. 点击下载 .CSV 文件,保存访问密钥秘密访问密钥,以供日后参考。
  12. 点击完成
  13. 选择权限标签页。
  14. 权限政策部分中,点击添加权限
  15. 选择添加权限
  16. 选择直接附加政策
  17. 搜索 AmazonS3FullAccess 政策。
  18. 选择相应政策。
  19. 点击下一步
  20. 点击添加权限

为 S3 上传配置 IAM 政策和角色

  1. AWS 控制台中,依次前往 IAM > 政策
  2. 依次点击创建政策 > JSON 标签页
  3. 复制并粘贴以下政策。
  4. 政策 JSON(如果您输入了其他存储桶名称,请替换 zendesk-crm-logs):

    {
      "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. 依次点击下一步 > 创建政策

  6. 依次前往 IAM > 角色 > 创建角色 > AWS 服务 > Lambda

  7. 附加新创建的政策。

  8. 将角色命名为 ZendeskCRMToS3Role,然后点击创建角色

创建 Lambda 函数

  1. AWS 控制台中,依次前往 Lambda > 函数 > 创建函数
  2. 点击从头开始创作
  3. 提供以下配置详细信息:

    设置
    名称 zendesk_crm_to_s3
    运行时 Python 3.13
    架构 x86_64
    执行角色 ZendeskCRMToS3Role
  4. 创建函数后,打开 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. 依次前往配置 > 环境变量

  6. 依次点击修改 > 添加新的环境变量

  7. 输入下表中提供的环境变量,并将示例值替换为您的值。

    环境变量

    示例值
    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. 创建函数后,请停留在其页面上(或依次打开 Lambda > 函数 > 您的函数)。

  9. 选择配置标签页。

  10. 常规配置面板中,点击修改

  11. 超时更改为 5 分钟(300 秒),然后点击保存

创建 EventBridge 计划

  1. 依次前往 Amazon EventBridge > 调度器 > 创建调度
  2. 提供以下配置详细信息:
    • 周期性安排费率 (1 hour)。
    • 目标:您的 Lambda 函数 zendesk_crm_to_s3
    • 名称zendesk_crm_to_s3-1h
  3. 点击创建时间表

(可选)为 Google SecOps 创建只读 IAM 用户和密钥

  1. 依次前往 AWS 控制台 > IAM > 用户 > 添加用户
  2. 点击 Add users(添加用户)。
  3. 提供以下配置详细信息:
    • 用户:输入 secops-reader
    • 访问类型:选择访问密钥 - 以程序化方式访问
  4. 点击创建用户
  5. 附加最低限度的读取政策(自定义):依次选择用户 > secops-reader > 权限 > 添加权限 > 直接附加政策 > 创建政策
  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. 名称 = secops-reader-policy

  8. 依次点击创建政策 > 搜索/选择 > 下一步 > 添加权限

  9. secops-reader 创建访问密钥:安全凭据 > 访问密钥

  10. 点击创建访问密钥

  11. 下载 .CSV。(您需要将这些值粘贴到 Feed 中)。

在 Google SecOps 中配置 Feed 以注入 Zendesk CRM 日志

  1. 依次前往 SIEM 设置> Feed
  2. 点击 + 添加新 Feed
  3. Feed 名称字段中,输入 Feed 的名称(例如 Zendesk CRM logs)。
  4. 选择 Amazon S3 V2 作为来源类型
  5. 选择 Zendesk CRM 作为日志类型
  6. 点击下一步
  7. 为以下输入参数指定值:
    • S3 URIs3://zendesk-crm-logs/zendesk/crm/
    • 来源删除选项:根据您的偏好选择删除选项。
    • 文件存在时间上限:包含在过去指定天数内修改的文件。默认值为 180 天。
    • 访问密钥 ID:有权访问 S3 存储桶的用户访问密钥。
    • 私有访问密钥:具有 S3 存储桶访问权限的用户私有密钥。
    • 资产命名空间资产命名空间
    • 注入标签:应用于此 Feed 中事件的标签。
  8. 点击下一步
  9. 最终确定界面中查看新的 Feed 配置,然后点击提交

需要更多帮助?从社区成员和 Google SecOps 专业人士那里获得解答。