收集 IBM Security Identity Manager 日志

支持范围:

本文档介绍了如何使用 Google Cloud Storage V2 将 IBM Security Identity Manager 日志注入 Google Security Operations。

IBM Security Identity Manager (ISIM) 是一种基于政策的自动化解决方案,可管理整个 IT 环境中的用户访问权限,有助于在整个企业中推动有效的身份管理和治理。通过使用角色、账号和访问权限,它可以在整个用户生命周期内自动创建、修改、重新认证和终止用户身份。在较新版本中,该产品也称为 IBM Security Verify Governance - Identity Manager。

准备工作

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

  • Google SecOps 实例
  • 启用了 Cloud Storage API 的 GCP 项目
  • 创建和管理 GCS 存储桶的权限
  • 管理 GCS 存储桶上的 IAM 政策的权限
  • 创建 Cloud Run 服务、Pub/Sub 主题和 Cloud Scheduler 作业的权限
  • 对 IBM Security Identity Manager REST API 的特权访问权限
  • IBM Security Identity Manager 7.0.2 或更高版本,或者 IBM Security Verify Governance 10.0.1 或更高版本
  • 具有访问 REST API 端点(/itim/rest/activities 和 /itim/rest/requests)的管理员权限的 ISIM 用户账号
  • GCP 项目与 ISIM 虚拟设备之间的网络连接(防火墙规则必须允许 ISIM 端口上的 HTTPS 流量)

配置 IBM Security Identity Manager API 访问权限

如需让 Google SecOps 能够检索活动和请求审核数据,您需要在 ISIM 中配置一个专用服务账号,并为其授予 REST API 访问权限所需的权限。

创建用于 API 访问的专用 ISIM 账号

  1. 登录 IBM Security Identity Manager 管理控制台。
  2. 依次前往管理用户 > 创建用户。
  3. 为集成创建新的用户账号(例如 secops-api-user)。
  4. 分配系统管理员角色或包含通过 REST API 查看活动和请求的权限的自定义角色。
  5. 安全地保存账号凭证:
    • 用户名:ISIM 登录用户名(例如 secops-api-user)
    • 密码:ISIM 登录密码

确定 ISIM API 基础网址

  • ISIM API 基础网址采用以下格式:

    https://<isim-hostname>:<port>
    
  • 例如,如果您的 ISIM 虚拟设备主机名为 isim.example.com,并且使用默认 HTTPS 端口:

    https://isim.example.com:9082
    

测试 API 访问权限

  • 在继续集成之前,请先测试您的凭证:

    # Replace with your actual credentials and ISIM URL
    ISIM_BASE_URL="https://isim.example.com:9082"
    ISIM_USER="chronicle-api-user"
    ISIM_PASS="your-password"
    
    # Authenticate and obtain session cookie
    curl -v -k -c cookies.txt \
        -d "j_username=${ISIM_USER}&j_password=${ISIM_PASS}" \
        "${ISIM_BASE_URL}/itim/j_security_check"
    
    # Retrieve CSRF token from systemusers/me endpoint
    curl -v -k -b cookies.txt \
        -H "Accept: application/json" \
        "${ISIM_BASE_URL}/itim/rest/systemusers/me"
    
    # Test activities endpoint
    curl -v -k -b cookies.txt \
        -H "Accept: application/json" \
        "${ISIM_BASE_URL}/itim/rest/activities"
    

成功的响应会返回一个 JSON 数组,其中包含最近的 ISIM 活动。

创建 Google Cloud Storage 存储桶

  1. 前往 Google Cloud 控制台。
  2. 选择您的项目或创建一个新项目。
  3. 在导航菜单中,前往 Cloud Storage > 存储桶。
  4. 点击创建存储分区。
  5. 提供以下配置详细信息:

    设置 值
    指定存储桶的名称 输入一个全局唯一的名称(例如 ibm-sim-logs-gcs)
    位置类型 根据您的需求进行选择(单区域、双区域、多区域)
    位置 选择位置(例如 us-central1)
    存储类别 标准(推荐用于频繁访问的日志)
    访问权限控制 统一(推荐)
    保护工具 可选:启用对象版本控制或保留政策
  6. 点击创建。

为 Cloud Run 函数创建服务账号

Cloud Run 函数需要一个服务账号,该账号应具有向 GCS 存储桶写入数据的权限,并且可由 Pub/Sub 调用。

创建服务账号

  1. 在 GCP 控制台中,前往 IAM 和管理 > 服务账号。
  2. 点击创建服务账号。
  3. 提供以下配置详细信息:
    • 服务账号名称:输入 ibm-sim-collector-sa
    • 服务账号说明:输入 Service account for Cloud Run function to collect IBM Security Identity Manager logs
  4. 点击创建并继续。
  5. 在为此服务账号授予项目访问权限部分,添加以下角色:
    1. 点击选择角色。
    2. 搜索并选择 Storage Object Admin。
    3. 点击 + 添加其他角色。
    4. 搜索并选择 Cloud Run Invoker。
    5. 点击 + 添加其他角色。
    6. 搜索并选择 Cloud Functions Invoker。
  6. 点击继续。
  7. 点击完成。

以下操作需要这些角色:

  • Storage Object Admin:将日志写入 GCS 存储桶并管理状态文件
  • Cloud Run Invoker:允许 Pub/Sub 调用函数
  • Cloud Functions Invoker:允许调用函数

授予对 GCS 存储桶的 IAM 权限

向服务账号授予对 GCS 存储桶的写入权限:

  1. 前往 Cloud Storage > 存储桶。
  2. 点击您的存储桶名称 (ibm-sim-logs-gcs)。
  3. 前往权限标签页。
  4. 点击授予访问权限。
  5. 提供以下配置详细信息:
    • 添加主账号:输入服务账号电子邮件地址 (ibm-sim-collector-sa@PROJECT_ID.iam.gserviceaccount.com)
    • 分配角色:选择 Storage Object Admin
  6. 点击保存。

创建 Pub/Sub 主题

创建一个 Pub/Sub 主题,Cloud Scheduler 将向其发布消息,Cloud Run 函数将订阅该主题。

  1. 在 GCP 控制台中,前往 Pub/Sub > 主题。
  2. 点击创建主题。
  3. 提供以下配置详细信息:
    • 主题 ID:输入 ibm-sim-logs-trigger
    • 其他设置均保留默认值
  4. 点击创建。

创建 Cloud Run 函数以收集日志

Cloud Run 函数将由来自 Cloud Scheduler 的 Pub/Sub 消息触发,以从 IBM Security Identity Manager REST API 提取日志并将其写入 GCS。

如需创建 Cloud Run 函数来收集日志,请执行以下操作:

  1. 在 GCP 控制台中,前往 Cloud Run。
  2. 点击创建服务。
  3. 选择函数(使用直接修改器创建函数)。
  4. 在配置部分中,提供以下配置详细信息:

    设置 值
    Service 名称 ibm-sim-collector
    区域 选择与您的 GCS 存储桶匹配的区域(例如 us-central1)
    运行时 选择 Python 3.12 或更高版本
  5. 在触发器(可选)部分中:

    1. 点击 + 添加触发器。
    2. 选择 Cloud Pub/Sub。
    3. 在选择 Cloud Pub/Sub 主题中,选择 ibm-sim-logs-trigger。
    4. 点击保存。
  6. 在身份验证部分中:

    1. 选择需要进行身份验证。
    2. 检查 Identity and Access Management (IAM)。
  7. 向下滚动并展开容器、网络、安全性。

  8. 前往安全标签页:

    • 服务账号:选择 ibm-sim-collector-sa
  9. 前往容器标签页:

    1. 点击变量和密钥。
    2. 针对每个环境变量,点击 + 添加变量:

      变量名称 示例值 说明
      GCS_BUCKET ibm-sim-logs-gcs GCS 存储桶名称
      GCS_PREFIX ibm-sim 日志文件的前缀
      STATE_KEY ibm-sim/state.json 状态文件路径
      ISIM_BASE_URL https://isim.example.com:9082 ISIM 服务器基础网址
      ISIM_USERNAME secops-api-user ISIM API 用户名
      ISIM_PASSWORD your-password ISIM API 密码
      LOOKBACK_HOURS 24 初始回溯期
      PAGE_SIZE 100 每个 API 页面上的记录数
      MAX_RECORDS 5000 每次运行的记录数上限
  10. 在变量和密钥部分中,向下滚动到请求:

    • 请求超时:输入 600 秒(10 分钟)
  11. 前往设置标签页:

    • 在资源部分中:
      • 内存:选择 512 MiB 或更高
      • CPU:选择 1
  12. 在修订版本伸缩部分中:

    • 实例数下限:输入 0
    • 实例数上限:输入 100
  13. 点击创建。

  14. 等待服务创建完成(1-2 分钟)。

  15. 创建服务后,内嵌代码编辑器会自动打开。

添加函数代码

  1. 在入口点字段中输入 main。
  2. 在内嵌代码编辑器中,创建两个文件:

    • main.py:

      import functions_framework
      from google.cloud import storage
      import json
      import os
      import urllib3
      import urllib.parse
      from datetime import datetime, timezone, timedelta
      
      # Disable SSL warnings for self-signed certificates
      urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
      
      http = urllib3.PoolManager(
          timeout=urllib3.Timeout(connect=10.0, read=60.0),
          retries=False,
          cert_reqs='CERT_NONE',
      )
      
      storage_client = storage.Client()
      
      GCS_BUCKET = os.environ.get('GCS_BUCKET')
      GCS_PREFIX = os.environ.get('GCS_PREFIX', 'ibm-sim')
      STATE_KEY = os.environ.get('STATE_KEY', 'ibm-sim/state.json')
      ISIM_BASE_URL = os.environ.get('ISIM_BASE_URL', '').rstrip('/')
      ISIM_USERNAME = os.environ.get('ISIM_USERNAME')
      ISIM_PASSWORD = os.environ.get('ISIM_PASSWORD')
      LOOKBACK_HOURS = int(os.environ.get('LOOKBACK_HOURS', '24'))
      PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '100'))
      MAX_RECORDS = int(os.environ.get('MAX_RECORDS', '5000'))
      
      @functions_framework.cloud_event
      def main(cloud_event):
          if not all([GCS_BUCKET, ISIM_BASE_URL, ISIM_USERNAME, ISIM_PASSWORD]):
              print('Error: Missing required environment variables')
              return
      
          try:
              bucket = storage_client.bucket(GCS_BUCKET)
              state = load_state(bucket)
              now = datetime.now(timezone.utc)
      
              if isinstance(state, dict) and state.get('last_event_time'):
                  try:
                      last_val = state['last_event_time']
                      if last_val.endswith('Z'):
                          last_val = last_val[:-1] + '+00:00'
                      last_time = datetime.fromisoformat(last_val)
                      last_time = last_time - timedelta(minutes=2)
                  except Exception as e:
                      print(f"Warning: Could not parse last_event_time: {e}")
                      last_time = now - timedelta(hours=LOOKBACK_HOURS)
              else:
                  last_time = now - timedelta(hours=LOOKBACK_HOURS)
      
              print(f"Fetching logs from {last_time.isoformat()} to {now.isoformat()}")
      
              session_cookies = authenticate()
              csrf_token = get_csrf_token(session_cookies)
      
              activities = fetch_activities(session_cookies, csrf_token, last_time, now)
              requests_data = fetch_requests(session_cookies, csrf_token, last_time, now)
      
              all_records = []
              for a in activities:
                  a['_isim_record_type'] = 'activity'
                  all_records.append(a)
              for r in requests_data:
                  r['_isim_record_type'] = 'request'
                  all_records.append(r)
      
              if not all_records:
                  print("No new records found.")
                  save_state(bucket, now.isoformat())
                  return
      
              timestamp = now.strftime('%Y%m%d_%H%M%S')
              object_key = f"{GCS_PREFIX}/ibm_sim_audit_{timestamp}.ndjson"
              blob = bucket.blob(object_key)
      
              ndjson = '\n'.join(
                  [json.dumps(r, ensure_ascii=False, default=str) for r in all_records]
              ) + '\n'
              blob.upload_from_string(ndjson, content_type='application/x-ndjson')
      
              print(f"Wrote {len(all_records)} records to gs://{GCS_BUCKET}/{object_key}")
      
              newest = find_newest_time(all_records)
              save_state(bucket, newest if newest else now.isoformat())
      
              print(f"Successfully processed {len(all_records)} records "
                  f"(activities: {len(activities)}, requests: {len(requests_data)})")
      
          except Exception as e:
              print(f'Error processing logs: {str(e)}')
              raise
      
      def authenticate():
          url = f"{ISIM_BASE_URL}/itim/j_security_check"
          encoded_body = urllib.parse.urlencode({
              'j_username': ISIM_USERNAME,
              'j_password': ISIM_PASSWORD
          }).encode('utf-8')
      
          response = http.request(
              'POST', url,
              body=encoded_body,
              headers={'Content-Type': 'application/x-www-form-urlencoded'},
              redirect=False
          )
      
          cookies = {}
          set_cookie_headers = response.headers.getlist('Set-Cookie') if hasattr(response.headers, 'getlist') else []
          if not set_cookie_headers:
              raw = response.headers.get('Set-Cookie', '')
              if raw:
                  set_cookie_headers = [raw]
      
          for cookie_header in set_cookie_headers:
              parts = cookie_header.split(';')[0]
              if '=' in parts:
                  name, value = parts.split('=', 1)
                  cookies[name.strip()] = value.strip()
      
          if not cookies:
              raise Exception(
                  f"Authentication failed: no session cookies returned "
                  f"(HTTP {response.status})"
              )
      
          print("Successfully authenticated to ISIM")
          return cookies
      
      def get_csrf_token(cookies):
          url = f"{ISIM_BASE_URL}/itim/rest/systemusers/me"
          cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()])
      
          response = http.request(
              'GET', url,
              headers={
                  'Accept': 'application/json',
                  'Cookie': cookie_str
              }
          )
      
          if response.status != 200:
              raise Exception(
                  f"Failed to get CSRF token: {response.status} - "
                  f"{response.data.decode('utf-8')}"
              )
      
          data = json.loads(response.data.decode('utf-8'))
          csrf_token = None
      
          if isinstance(data, dict):
              csrf_token = data.get('_csrf', data.get('CSRFToken'))
          if not csrf_token:
              csrf_header = response.headers.get('CSRFToken', '')
              if csrf_header:
                  csrf_token = csrf_header
      
          print("Successfully retrieved CSRF token")
          return csrf_token
      
      def fetch_activities(cookies, csrf_token, start_time, end_time):
          url = f"{ISIM_BASE_URL}/itim/rest/activities"
          cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()])
      
          headers = {
              'Accept': 'application/json',
              'Cookie': cookie_str
          }
          if csrf_token:
              headers['CSRFToken'] = csrf_token
      
          all_records = []
          start_index = 0
      
          while len(all_records) < MAX_RECORDS:
              params = {
                  '_start': str(start_index),
                  '_count': str(min(PAGE_SIZE, MAX_RECORDS - len(all_records)))
              }
              query_string = urllib.parse.urlencode(params)
              request_url = f"{url}?{query_string}"
      
              response = http.request('GET', request_url, headers=headers)
      
              if response.status != 200:
                  print(f"Activities query failed: {response.status} - "
                      f"{response.data.decode('utf-8')}")
                  break
      
              page_results = json.loads(response.data.decode('utf-8'))
      
              if isinstance(page_results, dict):
                  page_results = page_results.get('activities', page_results.get('items', []))
      
              if not page_results:
                  break
      
              all_records.extend(page_results)
              print(f"Activities: Retrieved {len(page_results)} records "
                  f"(total: {len(all_records)})")
      
              if len(page_results) < PAGE_SIZE:
                  break
      
              start_index += len(page_results)
      
          print(f"Total activities fetched: {len(all_records)}")
          return all_records
      
      def fetch_requests(cookies, csrf_token, start_time, end_time):
          url = f"{ISIM_BASE_URL}/itim/rest/requests"
          cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()])
      
          headers = {
              'Accept': 'application/json',
              'Cookie': cookie_str
          }
          if csrf_token:
              headers['CSRFToken'] = csrf_token
      
          all_records = []
          start_index = 0
      
          while len(all_records) < MAX_RECORDS:
              params = {
                  '_start': str(start_index),
                  '_count': str(min(PAGE_SIZE, MAX_RECORDS - len(all_records)))
              }
              query_string = urllib.parse.urlencode(params)
              request_url = f"{url}?{query_string}"
      
              response = http.request('GET', request_url, headers=headers)
      
              if response.status != 200:
                  print(f"Requests query failed: {response.status} - "
                      f"{response.data.decode('utf-8')}")
                  break
      
              page_results = json.loads(response.data.decode('utf-8'))
      
              if isinstance(page_results, dict):
                  page_results = page_results.get('requests', page_results.get('items', []))
      
              if not page_results:
                  break
      
              all_records.extend(page_results)
              print(f"Requests: Retrieved {len(page_results)} records "
                  f"(total: {len(all_records)})")
      
              if len(page_results) < PAGE_SIZE:
                  break
      
              start_index += len(page_results)
      
          print(f"Total requests fetched: {len(all_records)}")
          return all_records
      
      def find_newest_time(records):
          newest = None
          for r in records:
              for field in ['completedTime', 'scheduledTime', 'startedTime',
                          'lastModified', 'requestDate', 'timestamp']:
                  t = r.get(field)
                  if t:
                      try:
                          if isinstance(t, (int, float)):
                              dt = datetime.fromtimestamp(t / 1000, tz=timezone.utc)
                              t_iso = dt.isoformat()
                          else:
                              t_iso = str(t)
                          if newest is None or t_iso > newest:
                              newest = t_iso
                      except Exception:
                          continue
          return newest
      
      def load_state(bucket):
          try:
              blob = bucket.blob(STATE_KEY)
              if blob.exists():
                  return json.loads(blob.download_as_text())
          except Exception as e:
              print(f"Warning: Could not load state: {e}")
          return {}
      
      def save_state(bucket, last_event_time_iso):
          try:
              state = {
                  'last_event_time': last_event_time_iso,
                  'last_run': datetime.now(timezone.utc).isoformat()
              }
              blob = bucket.blob(STATE_KEY)
              blob.upload_from_string(
                  json.dumps(state, indent=2),
                  content_type='application/json'
              )
              print(f"Saved state: last_event_time={last_event_time_iso}")
          except Exception as e:
              print(f"Warning: Could not save state: {e}")
      
    • requirements.txt:

      functions-framework==3.*
      google-cloud-storage==2.*
      urllib3>=2.0.0
      
  3. 点击部署以保存和部署函数。

  4. 等待部署完成(2-3 分钟)。

创建 Cloud Scheduler 作业

Cloud Scheduler 将定期向 Pub/Sub 主题发布消息,从而触发 Cloud Run 函数。

  1. 在 GCP 控制台中,前往 Cloud Scheduler。
  2. 点击创建作业。
  3. 提供以下配置详细信息:

    设置 值
    名称 ibm-sim-collector-hourly
    区域 选择与 Cloud Run 函数相同的区域
    频率 0 * * * *(每小时整点)
    时区 选择时区(建议选择 UTC)
    目标类型 Pub/Sub
    主题 选择ibm-sim-logs-trigger
    邮件正文 {}(空 JSON 对象)
  4. 点击创建。

定时任务频率选项

根据日志量和延迟时间要求选择频率:

频率 Cron 表达式 使用场景
每隔 5 分钟 */5 * * * * 高容量、低延迟
每隔 15 分钟 */15 * * * * 搜索量中等
每小时 0 * * * * 标准(推荐)
每 6 小时 0 */6 * * * 小批量处理
每天 0 0 * * * 历史数据收集

测试集成

  1. 在 Cloud Scheduler 控制台中,找到您的作业 (ibm-sim-collector-hourly)。
  2. 点击强制运行以手动触发作业。
  3. 等待几秒钟。
  4. 前往 Cloud Run > 服务。
  5. 点击 ibm-sim-collector。
  6. 点击日志标签页。
  7. 验证函数是否已成功执行。查找:

    Fetching logs from YYYY-MM-DDTHH:MM:SS+00:00 to YYYY-MM-DDTHH:MM:SS+00:00
    Successfully authenticated to ISIM
    Successfully retrieved CSRF token
    Activities: Retrieved X records (total: X)
    Requests: Retrieved X records (total: X)
    Wrote X records to gs://ibm-sim-logs-gcs/ibm-sim/ibm_sim_audit_YYYYMMDD_HHMMSS.ndjson
    Successfully processed X records (activities: X, requests: X)
    
  8. 前往 Cloud Storage > 存储桶。

  9. 点击 ibm-sim-logs-gcs。

  10. 转到 ibm-sim/ 文件夹。

  11. 验证是否已创建带有当前时间戳的新 .ndjson 文件。

如果您在日志中看到错误,请执行以下操作:

  • HTTP 401/302:验证 ISIM_USERNAME 和 ISIM_PASSWORD 环境变量是否正确,以及用户账号是否未被锁定
  • HTTP 403:验证 ISIM 用户账号是否具有 REST API 访问权限的管理员权限
  • HTTP 404:验证 ISIM_BASE_URL 是否正确,并且包含正确的端口号
  • 连接错误:验证 Cloud Run 函数与 ISIM 虚拟设备之间的网络连接
  • 缺少环境变量:验证是否已在 Cloud Run 函数配置中设置所有必需变量

检索 Google SecOps 服务账号

Google SecOps 使用唯一服务账号从 GCS 存储桶中读取数据。您必须向此服务账号授予对存储桶的访问权限。

获取服务账号邮箱

  1. 依次前往设置 > 数据管理 > Feed。如果您使用的是仅 SIEM 平台,请依次前往设置 > Feed。
  2. 点击添加新新闻信息流。
  3. 点击配置单个 Feed。
  4. 在 Feed 名称字段中,输入 Feed 的名称(例如 IBM SIM Logs GCS)。
  5. 选择 Google Cloud Storage V2 作为来源类型。
  6. 选择 IBM Security Identity Manager 作为日志类型。
  7. 点击获取服务账号。系统会显示一个专属的服务账号邮箱,例如:

    secops-12345678@secops-gcp-prod.iam.gserviceaccount.com
    
  8. 复制此邮箱,以便在下一步中使用。

  9. 点击下一步。

  10. 为以下输入参数指定值:

    • 存储桶网址:输入带有前缀路径的 GCS 存储桶 URI:

      gs://ibm-sim-logs-gcs/ibm-sim/
      
    • 来源删除选项:根据您的偏好选择删除选项:

      • 永不:转移后永不删除任何文件(建议在测试时使用)。
      • 删除已转移的文件:在成功转移后删除文件。
      • 删除已转移的文件和空目录:在成功转移后删除文件和空目录。

    • 文件存在时间上限:包含过去指定天数内修改过的文件(默认值为 180 天)

    • 资产命名空间:资产命名空间

    • 注入标签:要应用于此 Feed 中事件的标签

  11. 点击下一步。

  12. 在最终确定界面中查看新的新闻信息流配置,然后点击提交。

向 Google SecOps 服务账号授予 IAM 权限

Google SecOps 服务账号需要对您的 GCS 存储桶拥有 Storage Object Viewer 角色。

如需向 Google SecOps 服务账号授予 IAM 权限,请执行以下操作:

  1. 前往 Cloud Storage > 存储桶。
  2. 点击 ibm-sim-logs-gcs。
  3. 前往权限标签页。
  4. 点击授予访问权限。
  5. 提供以下配置详细信息:
    • 添加主账号:粘贴 Google SecOps 服务账号邮箱
    • 分配角色:选择 Storage Object Viewer
  6. 点击保存。

UDM 映射表

日志字段 UDM 映射 逻辑
说明 metadata.description 事件的说明
receivedTime metadata.event_timestamp 事件发生时的时间戳
metadata.event_type metadata.event_type 事件类型
版本 metadata.product_version 产品版本
connectionId network.session_id 会话标识符
tls network.tls.version TLS 版本
ip principal.asset.ip 资产的 IP 地址
主机名 principal.hostname 主机名
ip principal.ip IP 地址
端口 principal.port 端口号
kv_data principal.user.attribute.labels 用户属性标签
操作 security_result.action_details 所采取措施的详细信息
metadata.product_name metadata.product_name 产品名称
metadata.vendor_name metadata.vendor_name 供应商名称

更新日志

查看此解析器的更新日志

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