收集 Proofpoint TAP Threats 日志

支持的平台:

本文档介绍了如何使用 Cloud Storage V2 将 Proofpoint TAP Threats 日志注入到 Google Security Operations。Proofpoint Targeted Attack Protection (TAP) 是一种先进的电子邮件安全平台,可检测、分析和阻止通过电子邮件传递的威胁,包括恶意附件和网址。TAP Threat API 会返回 TAP 在您的环境中发现的每种威胁的摘要:其分类、状态、严重程度、检测类型,以及与之关联的威胁主体、恶意软件系列、技术和品牌。Cloud Run 函数通过 TAP SIEM API 发现环境中观察到的威胁,并将其 Threat API 摘要写入 Cloud Storage 存储桶,Google SecOps 会从该存储桶中提取这些摘要。

准备工作

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

  • Google SecOps 实例
  • 启用了 Cloud Storage API 的 Google Cloud 项目
  • 创建和管理 Cloud Storage 存储桶的权限
  • 创建 Cloud Run 服务、Pub/Sub 主题和 Cloud Scheduler 作业的权限
  • 具有访问“威胁洞察信息中心”权限的 Proofpoint TAP 订阅
  • 具有 SIEM API 访问权限的 TAP API 服务凭据(服务主体和密钥)

生成 Proofpoint TAP API 服务凭证

  1. 登录 Proofpoint TAP Threat Insight Dashboard。
  2. 依次前往设置 > 已关联的应用 > 服务凭据。
  3. 点击创建新凭证。
  4. 在生成的服务凭证对话框中,复制并安全地存储以下内容:

    • 服务主体:用于 API 身份验证的主体标识符
    • 密钥:用于 API 身份验证的密钥

验证 API 访问权限

  • 在继续进行集成之前,请按以下步骤测试您的凭据:

    PRINCIPAL="your-service-principal"
    SECRET="your-secret"
    
    # Test SIEM API access (fetch last hour of events)
    curl -s "https://tap-api-v2.proofpoint.com/v2/siem/all?format=json&sinceSeconds=3600" \
      --user "${PRINCIPAL}:${SECRET}"
    

    如果响应成功,则会返回一个 JSON 对象,其中包含 messagesBlocked、messagesDelivered、clicksBlocked 和 clicksPermitted 数组。如果您收到 401 错误,请验证您的服务主体和密钥是否正确。如果您收到 403 错误,请确认您的账号已启用 TAP API 访问权限。

    然后,从该响应中的任意事件中获取 threatID 值,并测试 Threat API,该 API 是本指南所提取记录的来源:

    THREAT_ID="<threatID from a SIEM event>"
    
    curl -s "https://tap-api-v2.proofpoint.com/v2/threat/summary/${THREAT_ID}" \
      --user "${PRINCIPAL}:${SECRET}"
    

    成功的响应是一个 JSON 对象,其中包含 id、identifiedAt、name、type、category、status 以及 actors、families、malware、techniques 和 brands 数组。如果 SIEM 响应中没有事件,则表示环境在过去一小时内没有出现 TAP 识别的威胁;该函数仍会正常运行,但在出现威胁之前不会写入任何内容。

创建 Cloud Storage 存储桶

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

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

为 Cloud Run 函数创建服务账号

Cloud Run 函数需要一个服务账号,该账号具有写入 Cloud Storage 存储桶和被 Pub/Sub 调用的权限。

创建服务账号

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

以下操作需要这些角色:

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

授予对 Cloud Storage 存储桶的 Identity and Access Management (IAM) 权限

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

  1. 前往 Cloud Storage > 存储桶。
  2. 点击您的存储桶名称。
  3. 前往权限标签页。
  4. 点击授予访问权限。
  5. 提供以下配置详细信息:
    • 添加主账号:输入服务账号电子邮件地址(例如 tap-threats-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:输入 tap-threats-collector-trigger
    • 将其他设置保留为默认值
  4. 点击创建。

创建 Cloud Run 函数以收集威胁事件

  • Cloud Run 函数将由来自 Cloud Scheduler 的 Pub/Sub 消息触发。它会读取 TAP SIEM API 以查找在您的环境中发现的威胁 ID,从 TAP Threat API 中提取每种威胁的摘要,并将摘要写入 Cloud Storage。
  1. 在 GCP 控制台中,前往 Cloud Run。
  2. 点击编写函数。
  3. 在配置部分中,提供以下配置详细信息:

    设置 值
    Service 名称 tap-threats-collector
    区域 选择与您的 Cloud Storage 存储桶匹配的区域(例如 us-central1)
    运行时 选择 Python 3.12 或更高版本
  4. 在触发器部分中,点击添加触发器,然后选择 Pub/Sub 触发器。

  5. 在 Eventarc 触发器窗格中,提供以下配置详细信息:

    • 触发器名称:保留生成的名称,或输入触发器的名称。
    • 触发器类型:选择 Google 来源。
    • 事件提供方:选择 Pub/Sub。
    • 事件类型:选择 google.cloud.pubsub.topic.v1.messagePublished。
    • 选择 Cloud Pub/Sub 主题:选择主题 tap-threats-collector-trigger。
    • 区域:选择与函数相同的区域。
    • 服务账号:选择服务账号 tap-threats-collector-sa。
  6. 点击保存触发器。

  7. 在身份验证部分中:

    1. 选择需要进行身份验证。
    2. 选择 Identity and Access Management (IAM)。
  8. 前往并展开容器、网络、安全性。

  9. 前往安全标签页:

    • 服务账号:选择服务账号 tap-threats-collector-sa
  10. 前往容器标签页:

    1. 点击变量和密钥。
    2. 为每个环境变量点击 + 添加变量:
    变量名称 示例值 说明
    GCS_BUCKET proofpoint-tap-threats-logs Cloud Storage 存储桶名称
    GCS_PREFIX tap-threats 日志文件的前缀
    STATE_KEY tap-threats-state.json 状态路径,位于日志前缀之外
    TAP_PRINCIPAL your-service-principal TAP API Service Principal
    TAP_SECRET your-secret TAP API 密钥
    LOOKBACK_HOURS 24 首次运行时的初始发现窗口(以小时为单位,最大值为 168,即 SIEM API 限制)
    OVERLAP_MINUTES 10 重新读取水印之前的时间窗口,以捕获延迟编入索引的 SIEM 事件
    MAX_THREATS 500 每次运行提取的威胁摘要数量上限;其余摘要将保留在状态文件中排队
    SEEN_RETENTION_DAYS 7 威胁 ID 在上次被发现后被记住的天数
  11. 在变量和密钥部分中,前往请求:

    • 请求超时:输入 540 秒(9 分钟)
  12. 前往设置标签页:

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

    • 实例数下限:输入 0
    • 实例数上限:输入 100(或根据预期负载进行调整)
  14. 点击创建。

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

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

添加函数代码

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

    • 第一个文件 - main.py::
    import functions_framework
    from google.cloud import storage
    from google.cloud.exceptions import NotFound
    import base64
    import hashlib
    import json
    import os
    import urllib3
    from urllib.parse import quote
    from datetime import datetime, timezone, timedelta
    import time
    
    # Initialize HTTP client with timeouts
    http = urllib3.PoolManager(
        timeout=urllib3.Timeout(connect=5.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', 'tap-threats')
    # STATE_KEY must stay OUTSIDE GCS_PREFIX. The feed ingests every object under
    # its bucket URI and, with a deletion option selected, deletes what it
    # transferred. A state file inside the prefix would be ingested as log data and
    # then deleted, resetting collection and re-ingesting duplicates.
    STATE_KEY = os.environ.get('STATE_KEY', 'tap-threats-state.json')
    API_BASE = os.environ.get('TAP_API_BASE', 'https://tap-api-v2.proofpoint.com')
    TAP_PRINCIPAL = os.environ.get('TAP_PRINCIPAL')
    TAP_SECRET = os.environ.get('TAP_SECRET')
    # The SIEM API accepts at most 7 days of lookback.
    LOOKBACK_HOURS = min(int(os.environ.get('LOOKBACK_HOURS', '24')), 168)
    # The discovery query re-reads this many minutes before the watermark so that
    # events the SIEM API indexes late still surface their threat IDs.
    OVERLAP_MINUTES = int(os.environ.get('OVERLAP_MINUTES', '10'))
    # Maximum threat summaries fetched per run. The rest stay queued in the state.
    MAX_THREATS = int(os.environ.get('MAX_THREATS', '500'))
    # Days a threat ID stays in the state after it was last seen in SIEM events.
    SEEN_RETENTION_DAYS = int(os.environ.get('SEEN_RETENTION_DAYS', '7'))
    
    class FetchError(Exception):
        """Raised when a Proofpoint API call fails.
    
        The watermark must never advance on a failed fetch, otherwise the threats
        referenced in the failed window are never looked up.
        """
    
    def parse_datetime(value: str) -> datetime:
        """Parse an ISO 8601 datetime string into an aware datetime."""
        if value.endswith('Z'):
            value = value[:-1] + '+00:00'
        dt = datetime.fromisoformat(value)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    
    def tap_time(dt: datetime) -> str:
        """Render a datetime in the YYYY-MM-DDThh:mm:ssZ form the SIEM API accepts."""
        return dt.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
    
    def auth_header() -> str:
        """Build the HTTP Basic header from the service principal and secret."""
        token = base64.b64encode(f'{TAP_PRINCIPAL}:{TAP_SECRET}'.encode('utf-8')).decode('utf-8')
        return f'Basic {token}'
    
    def content_hash(record: dict) -> str:
        """Hash a threat summary so an unchanged summary is not written twice."""
        return hashlib.sha256(
            json.dumps(record, sort_keys=True, ensure_ascii=False).encode('utf-8')
        ).hexdigest()
    
    @functions_framework.cloud_event
    def main(cloud_event):
        """Discover threat IDs in TAP SIEM events and write their Threat API summaries.
    
        The PROOFPOINT_TAP_THREATS parser reads the Threat API summary schema, not the
        SIEM API events. The SIEM API is used only to find which threat IDs were
        observed; every message and click event for those threats is what the
        Proofpoint TAP (PROOFPOINT_MAIL) feed carries.
    
        Args:
            cloud_event: CloudEvent object containing the Pub/Sub message.
        """
        if not all([GCS_BUCKET, TAP_PRINCIPAL, TAP_SECRET]):
            # Raise rather than return: a bare return acks the Pub/Sub message and
            # reports the run as successful, silently discarding the schedule tick.
            raise RuntimeError('Missing required environment variables')
    
        bucket = storage_client.bucket(GCS_BUCKET)
        state = load_state(bucket, STATE_KEY)
    
        now = datetime.now(timezone.utc)
        watermark = None
        if state.get('last_event_time'):
            watermark = parse_datetime(state['last_event_time'])
        # threat_id -> {'hash': sha256 of the last written summary, 'seen': ISO time}
        seen = state.get('seen_threats', {})
        pending = list(state.get('pending_threat_ids', []))
    
        if watermark is None:
            start_time = now - timedelta(hours=LOOKBACK_HOURS)
        else:
            start_time = max(watermark - timedelta(minutes=OVERLAP_MINUTES), now - timedelta(days=7))
    
        headers = {
            'Authorization': auth_header(),
            'Accept': 'application/json',
            'User-Agent': 'GoogleSecOps-TAPThreatsCollector/2.0',
        }
    
        print(f"Discovering threats in SIEM events from {start_time.isoformat()} to {now.isoformat()}")
    
        # A FetchError here propagates: the run fails, the watermark is untouched,
        # and the next invocation retries the same window.
        discovered, newest_event_time = discover_threat_ids(headers, start_time, now)
    
        for threat_id in discovered:
            seen.setdefault(threat_id, {})['seen'] = now.isoformat()
            if threat_id not in pending:
                pending.append(threat_id)
        print(f"Discovered {len(discovered)} threat IDs, {len(pending)} queued for lookup")
    
        batch = pending[:MAX_THREATS]
        fresh = []
        for threat_id in batch:
            summary = fetch_threat_summary(headers, threat_id)
            if summary is None:
                # The Threat API has no summary for this ID; drop it from the queue.
                seen.setdefault(threat_id, {})['seen'] = now.isoformat()
                continue
            digest = content_hash(summary)
            if seen.get(threat_id, {}).get('hash') == digest:
                # Unchanged since it was last written; nothing new to ingest.
                continue
            fresh.append((threat_id, digest, summary))
        remaining = pending[len(batch):]
        if remaining:
            print(f"Reached max_threats limit ({MAX_THREATS}); {len(remaining)} threat IDs stay queued")
    
        # Forget threat IDs not seen for SEEN_RETENTION_DAYS so the state stays small.
        cutoff = now - timedelta(days=SEEN_RETENTION_DAYS)
        seen = {
            tid: info for tid, info in seen.items()
            if info.get('seen') and parse_datetime(info['seen']) >= cutoff
        }
    
        if fresh:
            timestamp = now.strftime('%Y%m%dT%H%M%SZ')
            object_key = f"{GCS_PREFIX}/threats_{timestamp}.ndjson"
            blob = bucket.blob(object_key)
            ndjson = '\n'.join(json.dumps(summary, ensure_ascii=False) for _, _, summary in fresh) + '\n'
            blob.upload_from_string(ndjson, content_type='application/x-ndjson')
            print(f"Wrote {len(fresh)} threat summaries to gs://{GCS_BUCKET}/{object_key}")
            for threat_id, digest, _ in fresh:
                seen.setdefault(threat_id, {})['hash'] = digest
        else:
            print("No new or changed threat summaries.")
    
        # Advance the watermark only after the data is durably written. When the
        # window held no events at all, keep the old watermark so a late-indexed
        # event is still picked up by the next overlap.
        new_watermark = watermark
        if newest_event_time:
            candidate = parse_datetime(newest_event_time)
            if new_watermark is None or candidate > new_watermark:
                new_watermark = candidate
    
        save_state(bucket, STATE_KEY, {
            'last_event_time': new_watermark.isoformat() if new_watermark else None,
            'seen_threats': seen,
            'pending_threat_ids': remaining,
        })
    
        print(f"Successfully processed {len(fresh)} threat summaries")
    
    def load_state(bucket, key):
        """Read the collector state from Cloud Storage.
    
        Only a missing object is treated as a cold start. Any other error is raised:
        swallowing it would silently reset collection to the full lookback window
        and re-ingest that entire period.
        """
        blob = bucket.blob(key)
        try:
            return json.loads(blob.download_as_text())
        except NotFound:
            print('No state file found. Starting from the lookback window.')
            return {}
    
    def save_state(bucket, key, state: dict):
        """Write the collector state to Cloud Storage.
    
        Failures are raised, not logged. If the state write fails after the data was
        uploaded, the next run repeats the same window and duplicates it.
        """
        blob = bucket.blob(key)
        blob.upload_from_string(
            json.dumps(state, indent=2),
            content_type='application/json',
        )
        print(f"Saved state: last_event_time={state.get('last_event_time')}, "
              f"{len(state.get('seen_threats', {}))} known threats, "
              f"{len(state.get('pending_threat_ids', []))} pending")
    
    def request_json(url: str, headers: dict):
        """Call a TAP API endpoint and return the decoded JSON body.
    
        Returns None on HTTP 404, which the Threat API uses for an unknown ID.
        Raises FetchError on any other transport or HTTP failure, so that the
        caller does not mistake a failed call for an empty result.
        """
        backoff = 1.0
        for attempt in range(5):
            try:
                response = http.request('GET', url, headers=headers)
            except Exception as e:
                raise FetchError(f'Request to {url} failed: {e}') from e
    
            if response.status == 429:
                raw_retry_after = response.headers.get('Retry-After')
                try:
                    # Retry-After may also be an HTTP date, which int() cannot parse.
                    wait = int(raw_retry_after) if raw_retry_after else int(backoff)
                except (TypeError, ValueError):
                    wait = int(backoff)
                print(f"Rate limited (429). Retrying after {wait}s...")
                time.sleep(wait)
                backoff = min(backoff * 2, 60.0)
                continue
    
            if response.status == 404:
                return None
    
            if response.status != 200:
                raise FetchError(f'HTTP {response.status} from {url}: {response.data.decode("utf-8", "replace")[:500]}')
    
            try:
                return json.loads(response.data.decode('utf-8'))
            except json.JSONDecodeError as e:
                raise FetchError(f'Malformed JSON response from {url}: {e}') from e
    
        raise FetchError('Rate limited repeatedly; giving up without advancing the watermark')
    
    def discover_threat_ids(headers: dict, start_time: datetime, end_time: datetime):
        """Read TAP SIEM events in one-hour windows and collect the threat IDs they reference.
    
        Returns:
            Tuple of (ordered list of unique threat IDs, newest event time ISO string).
        """
        threat_ids = []
        newest_time = None
        current_start = start_time
    
        while current_start < end_time:
            # The SIEM API allows at most one hour per request.
            current_end = min(current_start + timedelta(hours=1), end_time)
            interval = f'{tap_time(current_start)}/{tap_time(current_end)}'
            url = f'{API_BASE}/v2/siem/all?format=json&interval={quote(interval, safe="/:")}'
    
            data = request_json(url, headers)
            if data is None:
                raise FetchError(f'SIEM API returned 404 for interval {interval}')
    
            window_count = 0
            for key in ('messagesBlocked', 'messagesDelivered'):
                for message in data.get(key) or []:
                    window_count += 1
                    stamp = message.get('messageTime')
                    for info in message.get('threatsInfoMap') or []:
                        tid = info.get('threatID')
                        if tid and tid not in threat_ids:
                            threat_ids.append(tid)
                        stamp = stamp or info.get('threatTime')
                    if stamp and (newest_time is None or parse_datetime(stamp) > parse_datetime(newest_time)):
                        newest_time = stamp
            for key in ('clicksBlocked', 'clicksPermitted'):
                for click in data.get(key) or []:
                    window_count += 1
                    tid = click.get('threatID')
                    if tid and tid not in threat_ids:
                        threat_ids.append(tid)
                    stamp = click.get('clickTime') or click.get('threatTime')
                    if stamp and (newest_time is None or parse_datetime(stamp) > parse_datetime(newest_time)):
                        newest_time = stamp
    
            print(f"Interval {interval}: {window_count} events, {len(threat_ids)} unique threats so far")
            current_start = current_end
    
        return threat_ids, newest_time
    
    def fetch_threat_summary(headers: dict, threat_id: str):
        """Fetch one threat summary from the Threat API, or None if TAP has no summary for it."""
        url = f'{API_BASE}/v2/threat/summary/{quote(threat_id, safe="")}'
        summary = request_json(url, headers)
        if summary is None:
            print(f"Threat {threat_id}: no summary available (404)")
            return None
        if 'id' not in summary:
            summary['id'] = threat_id
        return summary
    

    • 第二个文件 - requirements.txt::
    functions-framework==3.*
    google-cloud-storage==2.*
    urllib3>=2.0.0
    
  3. 点击部署以保存并部署函数。

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

函数如何收集威胁

  • 该函数会从其水印开始,以一小时为间隔读取 TAP SIEM API /v2/siem/all 端点,并收集被屏蔽和已递送的邮件以及被屏蔽和允许的点击所引用的 threatID 值。系统不会写入 SIEM 事件本身,只会使用威胁 ID。
  • 对于每个发现的威胁 ID,它会调用 /v2/threat/summary/{threatId} 并将摘要写入为一条 NDJSON 记录。仅当总结的内容自上次撰写以来发生变化时,系统才会重新撰写总结,例如当威胁状态从 active 变为 cleared 时。
  • 状态文件会将最新的 SIEM 事件时间作为水印、每个威胁的上次写入摘要的哈希值,以及运行达到 MAX_THREATS 时仍在排队的威胁 ID。每次运行都会在水印之前重新读取 OVERLAP_MINUTES,并首先处理队列,因此当运行达到上限或事件索引延迟时,不会丢失任何内容。
  • 任何 API 或存储错误都会导致运行失败,并使水印保持不变,因此下次运行会重试同一时间窗口。

创建 Cloud Scheduler 作业

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

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

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

时间表频率选项

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

频率 Cron 表达式 使用场景
每隔 15 分钟 */15 * * * * 威胁众多的高流量环境
每小时 0 * * * * 标准(推荐)
每 6 小时 0 */6 * * * 低容量环境

测试集成

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

    Discovering threats in SIEM events from YYYY-MM-DDTHH:MM:SS+00:00 to YYYY-MM-DDTHH:MM:SS+00:00
    Interval .../...: X events, Y unique threats so far
    Discovered Y threat IDs, Y queued for lookup
    Wrote Z threat summaries to gs://proofpoint-tap-threats-logs/tap-threats/threats_YYYYMMDDTHHMMSSZ.ndjson
    Successfully processed Z threat summaries
    
  8. 前往 Cloud Storage > 存储桶。

  9. 点击您的存储桶名称。

  10. 前往前缀文件夹 tap-threats/。

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

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

  • HTTP 401:检查环境变量中的 TAP_PRINCIPAL 和 TAP_SECRET。验证服务主体和密钥是否正确。
  • HTTP 403:确认您的 TAP 账号已启用 API 访问权限。
  • HTTP 429:速率限制 - 函数将自动重试并进行退避。请考虑降低时间表频率。
  • 没有新的或更改的威胁摘要:如果在相应时间范围内未观察到任何威胁,或者每个观察到的威胁都已写入相同的内容,则属于正常情况。TAP 仅报告由 URL 防御或附件防御功能识别的威胁。
  • 缺少环境变量:检查是否已设置所有必需的变量。

在 Google SecOps 中配置 Feed 以注入 Proofpoint TAP Threats 日志

  1. 前往 SIEM 设置 > Feed。
  2. 点击添加新 Feed。
  3. 点击配置单个 Feed。
  4. 在 Feed 名称字段中,输入 Feed 的名称(例如 Proofpoint TAP Threats)。
  5. 选择 Google Cloud Storage V2 作为来源类型。
  6. 选择 Proofpoint Tap Threats 作为日志类型。
  7. 点击获取服务账号。系统会显示一个专属的服务账号邮箱,例如:

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

  9. 点击下一步。

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

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

      gs://proofpoint-tap-threats-logs/tap-threats/
      
      • 替换:
        • proofpoint-tap-threats-logs:您的 Cloud Storage 存储桶名称。
        • tap-threats:存储日志的可选前缀或文件夹路径(留空表示根目录)。
    • 来源删除选项:根据您的偏好选择删除选项:
      • 永不删除文件:永不从源中删除文件(建议用于测试)。
      • 删除已转移的文件和空目录:在成功完成提取后,从来源位置删除文件和空目录。
    • 文件存在时间上限:包含在过去指定天数内修改过的文件(默认值为 180 天)
    • 资产命名空间:资产命名空间
    • 注入标签:要应用于此 Feed 中事件的标签
  11. 点击下一步。

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

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

Google SecOps 服务账号需要对您的 Cloud Storage 存储桶拥有两个角色:用于读取日志对象的 Storage Object Viewer,以及用于读取存储桶元数据的存储桶级角色。

  1. 前往 Cloud Storage > 存储桶。
  2. 点击您的存储桶名称。
  3. 前往权限标签页。
  4. 点击授予访问权限。
  5. 提供以下配置详细信息:
    • 添加主账号:粘贴 Google SecOps 服务账号电子邮件地址
    • 分配角色:选择以下两个角色:
      • Storage Object Viewer:读取日志对象。
      • Storage Legacy Bucket Reader:读取存储桶元数据。如果您选择了删除已转移的文件和空目录删除选项,请改为选择存储旧版存储桶写入者,该角色也会授予删除权限。
  6. 点击保存。

UDM 映射表

日志字段 UDM 映射 逻辑
detectionType_label additional.fields 已合并
geoTargeted_label additional.fields 已合并
notable_label additional.fields 已合并
verticallyTargeted_label additional.fields 已合并
identifiedAt metadata.event_timestamp 已解析为 ISO8601
has_principal metadata.event_type 已映射:true → NETWORK_CONNECTION,true → STATUS_UPDATE
type metadata.product_event_type 直接映射
id metadata.product_log_id 直接映射
_associations security_result.associations 已合并
_techniques security_result.attack_details.techniques 已合并
_category security_result.category 已合并
_category_details security_result.category_details 已合并
notable security_result.priority 已映射:true → HIGH_PRIORITY
name security_result.threat_name 直接映射
status security_result.threat_status 已映射:active → ACTIVE,cleared → CLEARED
_verdict_info security_result.verdict_info 已合并
不适用 metadata.event_type 常量:NETWORK_CONNECTION
不适用 metadata.product_event_type 常量:Proofpoint_Threats_Feed
不适用 metadata.product_name 常量:TAP Threats
不适用 metadata.vendor_name 常量:Proofpoint
不适用 security_result.priority 常量:HIGH_PRIORITY
不适用 security_result.threat_status 常量:ACTIVE

更新日志

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

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