收集 Oracle NetSuite - NetSuite Applications Suite 日志

支持的平台:

本文档介绍了如何使用 Google Cloud Storage V2 将 Oracle NetSuite - NetSuite Applications Suite 日志注入到 Google Security Operations。

Oracle NetSuite 是一个基于云的企业资源规划 (ERP) 平台,可提供全面的业务管理功能,包括财务管理、客户关系管理 (CRM)、电子商务和库存管理。NetSuite 会生成广泛的审核日志,包括用户登录轨迹、跟踪记录更改的系统备注和 API 访问日志,这些日志可通过 SuiteQL REST API 进行检索,以用于安全监控和合规性目的。

准备工作

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

  • Google SecOps 实例
  • 启用了 Cloud Storage、Cloud Run、Pub/Sub 和 Cloud Scheduler API 的 GCP 项目
  • 创建和管理 GCS 存储分区的权限
  • 管理 GCS 存储分区的 IAM 政策的权限
  • 创建 Cloud Run 服务、Pub/Sub 主题和 Cloud Scheduler 作业的权限
  • 具有管理员访问权限的 NetSuite 账号
  • 已启用 NetSuite REST Web 服务
  • 在 NetSuite 中创建集成记录和访问令牌的权限

配置 NetSuite API 访问权限

如需通过 SuiteQL REST API 启用日志收集,您必须在 NetSuite 中启用 REST Web 服务、创建集成记录并生成基于令牌的身份验证 (TBA) 凭据。

启用必需的 NetSuite 功能

  1. 以管理员身份登录您的 NetSuite 账号。
  2. 依次前往设置 > 公司 > 启用功能
  3. 点击 SuiteCloud 标签页。
  4. SuiteScript 部分,勾选以下复选框:
    • 客户端 SuiteScript
    • 服务器 SuiteScript
  5. SuiteTalk(Web 服务)部分,选中以下复选框:
    • REST Web 服务
  6. 管理身份验证部分,选中基于令牌的身份验证复选框。
  7. 在 SuiteCloud 服务条款页面上,点击我同意
  8. 点击保存

创建集成记录

  1. 在 NetSuite 中,依次前往设置 > 集成 > 管理集成 > 新建
  2. 名称字段中,输入 Google SecOps Integration
  3. 说明字段中,输入 Integration for Google Security Operations log collection
  4. 状态字段中,保留默认的已启用选项。
  5. 点击 Authentication(身份验证)标签。
  6. 基于令牌的身份验证部分中,选中基于令牌的身份验证框。
  7. TBA:授权流程复选框保留为未选中状态。
  8. 勾选 TBA:ISSUETOKEN 端点复选框。
  9. 点击保存
  10. 在确认页面上,复制并保存使用方密钥/客户端 ID使用方密钥/客户端密钥

创建用于 API 访问的自定义角色

  1. 依次前往设置 > 用户/角色 > 管理角色 > 新建
  2. 名称字段中,输入 Google SecOps API Role
  3. ID 字段中,输入 google_secops_api_role
  4. 点击权限子标签页。
  5. 点击设置子标签页。
  6. 添加以下权限,并具有完全访问权限级别:
    • 使用访问令牌登录
    • 用户访问令牌
    • REST Web 服务
  7. 点击报告子标签页。
  8. 添加以下权限,并具有查看访问权限级别:
    • SuiteAnalytics 工作簿
  9. 点击列表子标签页。
  10. 添加以下权限,并具有查看访问权限级别:
    • 登录审核跟踪记录
  11. 点击保存

为用户分配角色

  1. 依次前往列表 > 员工 > 员工
  2. 选择将用于集成的用户账号(或创建新的专用集成用户)。
  3. 点击修改
  4. 点击访问权限标签页。
  5. 角色部分,点击添加
  6. 从下拉列表中选择 Google SecOps API 角色
  7. 点击保存

创建访问令牌

  1. 以分配了 Google SecOps API 角色的用户身份登录 NetSuite。
  2. 依次前往设置 > 用户/角色 > 访问令牌 > 新建
  3. 访问令牌页面上:
    • 应用名称下拉列表中,选择 Google SecOps Integration
    • 用户下拉列表中,选择分配了 Google SecOps API 角色的用户账号。
    • 角色下拉列表中,选择 Google SecOps API 角色
    • 系统会自动填充令牌名称字段。您可以根据需要修改此设置。
  4. 点击保存
  5. 在确认页面上,复制并保存 Token IDToken Secret

查找您的 NetSuite 账号 ID

  1. 在 NetSuite 中,依次前往设置 > 公司 > 公司信息
  2. 找到账号 ID 字段(显示在时区设置下方)。
  3. 复制并保存您的账号 ID(例如,沙盒账号的 ID 为 12345671234567_SB1)。

验证权限

如需验证账号是否具有所需权限,请执行以下操作:

  1. 使用分配了 Google SecOps API 角色的用户账号登录 NetSuite。
  2. 依次前往设置 > 用户/角色 > 查看登录审核轨迹
  3. 如果您能看到“登录审核轨迹”页面,则表示相应账号拥有所需的权限。
  4. 如果您看不到此选项,请与 NetSuite 管理员联系,以在列表子标签页下授予登录审核轨迹权限。

测试 API 访问权限

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

    # Replace with your actual values
    ACCOUNT_ID="your-account-id"
    
    # Replace underscores with hyphens in the account ID for the URL
    # For example: 1234567_SB1 becomes 1234567-sb1
    URL_ACCOUNT_ID=$(echo "$ACCOUNT_ID" | tr '_' '-' | tr '[:upper:]' '[:lower:]')
    
    CONSUMER_KEY="your-consumer-key"
    CONSUMER_SECRET="your-consumer-secret"
    TOKEN_ID="your-token-id"
    TOKEN_SECRET="your-token-secret"
    
    # Test SuiteQL API access using OAuth 1.0 TBA
    # Note: This example requires the oauth1 CLI tool or Postman.
    # Use the following endpoint and headers to verify connectivity:
    # POST https://${URL_ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=5
    # Headers:
    #   Authorization: OAuth (with consumer key, token, HMAC-SHA256 signature)
    #   Content-Type: application/json
    #   Prefer: transient
    # Body: {"q": "SELECT id, date, status, user, emailaddress FROM LoginAudit ORDER BY date DESC"}
    

成功的响应会返回一个包含 LoginAudit 记录的 JSON 对象。

创建 Google Cloud Storage 存储桶

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

    设置
    为存储桶命名 输入一个全局唯一的名称(例如 netsuite-audit-logs
    位置类型 根据您的需求进行选择(区域级、双区域、多区域)
    位置 选择营业地点(例如 us-central1
    存储类别 标准(建议用于经常访问的日志)
    访问权限控制 均匀(推荐)
    保护工具 可选:启用对象版本控制或保留政策
  6. 点击创建

为 Cloud Run 函数创建服务账号

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

创建服务账号

  1. GCP 控制台中,依次前往 IAM 和管理 > 服务账号
  2. 点击创建服务账号
  3. 提供以下配置详细信息:
    • 服务账号名称:输入 netsuite-audit-collector-sa
    • 服务账号说明:输入 Service account for Cloud Run function to collect NetSuite audit 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 权限

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

创建 Pub/Sub 主题

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

创建 Cloud Run 函数以收集日志

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

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

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

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

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

  8. 前往安全性标签页:

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

    1. 点击变量和密钥
    2. 为每个环境变量点击+ 添加变量
    变量名称 示例值 说明
    GCS_BUCKET netsuite-audit-logs GCS 存储桶名称
    GCS_PREFIX netsuite-audit 日志文件的前缀
    STATE_KEY netsuite-audit/state.json 状态文件路径
    NS_ACCOUNT_ID 1234567 NetSuite 账号 ID
    NS_CONSUMER_KEY your-consumer-key 集成记录中的使用方密钥
    NS_CONSUMER_SECRET your-consumer-secret 集成记录中的使用方密钥
    NS_TOKEN_ID your-token-id 来自访问令牌的令牌 ID
    NS_TOKEN_SECRET your-token-secret 来自访问令牌的令牌密钥
    MAX_RECORDS 10000 每次运行的记录数上限
    PAGE_SIZE 1000 每个 SuiteQL 页面的记录数
    LOOKBACK_HOURS 24 初始回溯期
  10. 变量和 Secret 部分中,向下滚动到请求

    • 请求超时:输入 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 hashlib
    import hmac
    import base64
    import time
    import secrets
    import urllib.parse
    from datetime import datetime, timezone, timedelta
    
    # Initialize HTTP client with timeouts
    http = urllib3.PoolManager(
      timeout=urllib3.Timeout(connect=10.0, read=60.0),
      retries=False,
    )
    
    # Initialize Storage client
    storage_client = storage.Client()
    
    # Environment variables
    GCS_BUCKET = os.environ.get('GCS_BUCKET')
    GCS_PREFIX = os.environ.get('GCS_PREFIX', 'netsuite-audit')
    STATE_KEY = os.environ.get('STATE_KEY', 'netsuite-audit/state.json')
    NS_ACCOUNT_ID = os.environ.get('NS_ACCOUNT_ID')
    NS_CONSUMER_KEY = os.environ.get('NS_CONSUMER_KEY')
    NS_CONSUMER_SECRET = os.environ.get('NS_CONSUMER_SECRET')
    NS_TOKEN_ID = os.environ.get('NS_TOKEN_ID')
    NS_TOKEN_SECRET = os.environ.get('NS_TOKEN_SECRET')
    MAX_RECORDS = int(os.environ.get('MAX_RECORDS', '10000'))
    PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '1000'))
    LOOKBACK_HOURS = int(os.environ.get('LOOKBACK_HOURS', '24'))
    
    def get_netsuite_base_url(account_id):
      """Build the NetSuite REST API base URL from the account ID."""
      url_account = account_id.replace('_', '-').lower()
      return f"https://{url_account}.suitetalk.api.netsuite.com"
    
    def generate_oauth_header(method, url, account_id, consumer_key,
      consumer_secret, token_id, token_secret):
      """Generate OAuth 1.0 Authorization header using HMAC-SHA256."""
      timestamp = str(int(time.time()))
      nonce = secrets.token_hex(16)
    
      # Parse URL to separate base URL and query parameters
      parsed = urllib.parse.urlparse(url)
      base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
      query_params = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
    
      # Build parameter string (OAuth params + query params)
      params = {
        'oauth_consumer_key': consumer_key,
        'oauth_token': token_id,
        'oauth_nonce': nonce,
        'oauth_timestamp': timestamp,
        'oauth_signature_method': 'HMAC-SHA256',
        'oauth_version': '1.0',
      }
      for key, values in query_params.items():
        params[key] = values[0]
    
      # Sort parameters and create parameter string
      sorted_params = sorted(params.items())
      param_string = '&'.join(
        f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(v, safe='')}"
        for k, v in sorted_params
      )
    
      # Create signature base string
      signature_base = (
        f"{method.upper()}&"
        f"{urllib.parse.quote(base_url, safe='')}&"
        f"{urllib.parse.quote(param_string, safe='')}"
      )
    
      # Create signing key
      signing_key = (
        f"{urllib.parse.quote(consumer_secret, safe='')}&"
        f"{urllib.parse.quote(token_secret, safe='')}"
      )
    
      # Generate HMAC-SHA256 signature
      signature = base64.b64encode(
        hmac.new(
          signing_key.encode('utf-8'),
          signature_base.encode('utf-8'),
          hashlib.sha256
        ).digest()
      ).decode('utf-8')
    
      # Build realm from account ID
      realm = account_id.replace('-', '_').upper()
    
      # Build Authorization header
      auth_header = (
        f'OAuth realm="{realm}",'
        f'oauth_consumer_key="{consumer_key}",'
        f'oauth_token="{token_id}",'
        f'oauth_nonce="{nonce}",'
        f'oauth_timestamp="{timestamp}",'
        f'oauth_signature_method="HMAC-SHA256",'
        f'oauth_version="1.0",'
        f'oauth_signature="{urllib.parse.quote(signature, safe="")}"'
      )
      return auth_header
    
    @functions_framework.cloud_event
    def main(cloud_event):
      """
      Cloud Run function triggered by Pub/Sub to fetch NetSuite
      LoginAudit logs via SuiteQL and write to GCS.
      """
      if not all([GCS_BUCKET, NS_ACCOUNT_ID, NS_CONSUMER_KEY,
        NS_CONSUMER_SECRET, NS_TOKEN_ID, NS_TOKEN_SECRET]):
        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()}")
    
        records, newest_time = fetch_login_audit(last_time, now)
    
        if not records:
          print("No new log records found.")
          save_state(bucket, now.isoformat())
          return
    
        timestamp = now.strftime('%Y%m%d_%H%M%S')
        object_key = f"{GCS_PREFIX}/netsuite_audit_{timestamp}.ndjson"
        blob = bucket.blob(object_key)
    
        ndjson = '\n'.join(
          [json.dumps(r, ensure_ascii=False, default=str) for r in records]
        ) + '\n'
        blob.upload_from_string(ndjson, content_type='application/x-ndjson')
    
        print(f"Wrote {len(records)} records to gs://{GCS_BUCKET}/{object_key}")
    
        if newest_time:
          save_state(bucket, newest_time)
        else:
          save_state(bucket, now.isoformat())
    
        print(f"Successfully processed {len(records)} records")
    
      except Exception as e:
        print(f'Error processing logs: {str(e)}')
        raise
    
    def fetch_login_audit(start_time, end_time):
      """
      Fetch LoginAudit records from NetSuite SuiteQL API with pagination.
    
      Returns:
        Tuple of (records list, newest_event_time ISO string)
      """
      base_url = get_netsuite_base_url(NS_ACCOUNT_ID)
      suiteql_endpoint = f"{base_url}/services/rest/query/v1/suiteql"
    
      start_str = start_time.strftime('%Y-%m-%d %H:%M:%S')
      end_str = end_time.strftime('%Y-%m-%d %H:%M:%S')
    
      query = (
        "SELECT id, date, status, user, role, emailaddress, "
        "ipaddress, useragent, detail, requesturi, "
        "oauthaccesstokenname, oauthappname "
        "FROM LoginAudit "
        f"WHERE date BETWEEN TO_DATE('{start_str}', 'YYYY-MM-DD HH24:MI:SS') "
        f"AND TO_DATE('{end_str}', 'YYYY-MM-DD HH24:MI:SS') "
        "ORDER BY date ASC"
      )
    
      records = []
      newest_time = None
      offset = 0
      backoff = 1.0
    
      while True:
        if len(records) >= MAX_RECORDS:
          print(f"Reached max_records limit ({MAX_RECORDS})")
          break
    
        remaining = min(PAGE_SIZE, MAX_RECORDS - len(records))
        url = f"{suiteql_endpoint}?limit={remaining}&offset={offset}"
    
        auth_header = generate_oauth_header(
          'POST', url, NS_ACCOUNT_ID, NS_CONSUMER_KEY,
          NS_CONSUMER_SECRET, NS_TOKEN_ID, NS_TOKEN_SECRET
        )
    
        headers = {
          'Authorization': auth_header,
          'Content-Type': 'application/json',
          'Prefer': 'transient',
          'User-Agent': 'GoogleSecOps-NetSuiteCollector/1.0',
        }
    
        body = json.dumps({'q': query}).encode('utf-8')
    
        try:
          response = http.request('POST', url, headers=headers, body=body)
    
          if response.status == 429:
            retry_after = int(
              response.headers.get('Retry-After', str(int(backoff)))
            )
            print(f"Rate limited (429). Retrying after {retry_after}s...")
            time.sleep(retry_after)
            backoff = min(backoff * 2, 60.0)
            continue
    
          backoff = 1.0
    
          if response.status != 200:
            print(f"HTTP Error: {response.status}")
            response_text = response.data.decode('utf-8')
            print(f"Response body: {response_text}")
            return records, newest_time
    
          data = json.loads(response.data.decode('utf-8'))
    
          page_results = data.get('items', [])
    
          if not page_results:
            print("No more results (empty page)")
            break
    
          page_num = (offset // PAGE_SIZE) + 1
          print(f"Page {page_num}: Retrieved {len(page_results)} records")
          records.extend(page_results)
    
          for event in page_results:
            try:
              event_time = event.get('date')
              if event_time:
                if newest_time is None or event_time > newest_time:
                  newest_time = event_time
            except Exception as e:
              print(f"Warning: Could not parse event time: {e}")
    
          has_more = data.get('hasMore', False)
          if not has_more:
            print("Reached last page (hasMore=false)")
            break
    
          offset += len(page_results)
    
        except Exception as e:
          print(f"Error fetching logs: {e}")
          return records, newest_time
    
      print(f"Retrieved {len(records)} total records")
      return records, newest_time
    
    def load_state(bucket):
      """Load state from GCS."""
      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):
      """Save the last event timestamp to GCS state file."""
      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 Console 中,前往 Cloud Scheduler
  2. 点击创建作业
  3. 提供以下配置详细信息:

    设置
    名称 netsuite-audit-collector-hourly
    区域 选择与 Cloud Run 函数相同的区域
    频率 0 * * * *(每小时一次,整点时)
    时区 选择时区(建议选择世界协调时间 [UTC])
    目标类型 Pub/Sub
    主题 选择netsuite-audit-trigger
    消息正文 {}(空 JSON 对象)
  4. 点击创建

时间表频率选项

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

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

测试集成

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

    Fetching logs from YYYY-MM-DDTHH:MM:SS+00:00 to YYYY-MM-DDTHH:MM:SS+00:00
    Page 1: Retrieved X records
    Wrote X records to gs://netsuite-audit-logs/netsuite-audit/netsuite_audit_YYYYMMDD_HHMMSS.ndjson
    Successfully processed X records
    
  8. 前往 Cloud Storage > 存储分区

  9. 点击 netsuite-audit-logs

  10. 转到 netsuite-audit/ 文件夹。

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

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

  • HTTP 401:验证 OAuth 凭据(NS_CONSUMER_KEYNS_CONSUMER_SECRETNS_TOKEN_IDNS_TOKEN_SECRET)和 NS_ACCOUNT_ID 环境变量是否正确
  • HTTP 403:验证 NetSuite 角色是否具有所需的权限(REST Web 服务、登录审核跟踪记录、SuiteAnalytics 工作簿)
  • HTTP 429:速率限制 - 函数将自动重试,并采用指数退避算法
  • 缺少环境变量:验证是否已在 Cloud Run 函数配置中设置所有必需的变量

检索 Google SecOps 服务账号

  1. 依次前往 SIEM 设置 > Feed
  2. 点击添加新 Feed
  3. 点击配置单个 Feed
  4. Feed 名称字段中,输入 Feed 的名称(例如 NetSuite Audit Logs)。
  5. 选择 Google Cloud Storage V2 作为来源类型
  6. 选择 Oracle NetSuite - NetSuite Applications Suite 作为日志类型
  7. 点击获取服务账号。系统会显示一个唯一的服务账号电子邮件地址。例如:

    chronicle-12345678@chronicle-gcp-prod.iam.gserviceaccount.com
    
  8. 复制此电子邮件地址,以便在下一步中使用。

  9. 点击下一步

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

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

      gs://netsuite-audit-logs/netsuite-audit/
      
    • 来源删除选项:根据您的偏好选择删除选项:
      • 永不:转移后永不删除任何文件(建议用于测试)。
      • 删除已转移的文件:在成功转移后删除文件。
      • 删除已转移的文件和空目录:成功转移后删除文件和空目录。
    • 文件存在时间上限:包含在过去指定天数内修改的文件(默认值为 180 天)
    • 资产命名空间资产命名空间
    • 注入标签:要应用于此 Feed 中事件的标签
  11. 点击下一步

  12. 最终确定界面中查看新的 Feed 配置,然后点击提交

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

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

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

NetSuite API 速率限制

NetSuite API 具有以下并发和速率限制:

限制类型 默认值 备注
并发请求数 15 个请求 适用于每个账号的 SOAP 和 REST API 请求总数
SuiteQL 查询结果 最多 10 万行 每个 SuiteQL 查询返回的行数上限
每次请求的对象数量 最多 1,000 个对象 每个 API 请求返回的对象数量上限

Cloud Run 函数会自动处理指数退避算法速率限制。如果您遇到持续的速率限制错误 (HTTP 429),请与 NetSuite 支持团队联系,以提高您的 API 限制或调整 Cloud Scheduler 频率。

UDM 映射表

日志字段 UDM 映射 逻辑
用户 extensions.auth.type 所用身份验证机制的类型
日期 metadata.event_timestamp 事件发生时的时间戳
event_type metadata.event_type 事件类型(例如 USER_LOGIN、NETWORK_CONNECTION)
security_result security_result 有关事件安全结果的详细信息
oauthaccesstokenname security_result.about.resource.attribute.labels 描述安全结果所涉及资源属性的键值对标签
secchallenge security_result.about.resource.attribute.labels
状态 security_result.summary 安全性结果的简要说明
oauthappname target.application 目标应用的名称
用户代理 target.application
ipaddress target.ip 目标的 IP 地址
requesturi target.url 目标资源的网址
emailaddress target.user.email_addresses 与目标用户关联的电子邮件地址
角色 target.user.role_name 分配给目标用户的角色名称
用户 target.user.userid 目标用户的唯一标识符

更新日志

查看相应解析器的更改日志

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