收集 BeyondTrust Endpoint Privilege Management (EPM) 日志

支持的平台:

本文档介绍了如何使用 Cloud Storage 将 BeyondTrust Endpoint Privilege Management (EPM) 日志注入到 Google Security Operations。该解析器专注于将 BeyondTrust Endpoint 的原始 JSON 日志数据转换为符合 Google SecOps UDM 的结构化格式。它首先初始化各个字段的默认值,然后解析 JSON 载荷,随后将原始日志中的特定字段映射到 event.idm.read_only_udm 对象中的相应 UDM 字段。

准备工作

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

  • Google SecOps 实例
  • 启用了 Cloud Storage API 的 Google Cloud 项目
  • 创建和管理 Cloud Storage 存储分区的权限
  • 管理 Cloud Storage 存储分区的 Identity and Access Management (IAM) 政策的权限
  • 创建 Cloud Run 函数服务、Pub/Sub 主题和 Cloud Scheduler 作业的权限
  • 对 BeyondTrust Endpoint Privilege Management 租户或 API 的特权访问权限

创建 Cloud Storage 存储桶

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

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

收集 BeyondTrust EPM API 凭据

  1. 以管理员身份登录 BeyondTrust Privilege Management Web 控制台。
  2. 依次前往配置 > 设置 > API 设置
  3. 点击 Create an API Account
  4. 提供以下配置详细信息:
    • 名称:输入 Google SecOps Collector
    • API 访问权限:启用报告(只读)。此收集器使用的 /management-api/v3/Events/FromStartDate 端点属于报告权限类别。如果您还打算收集 Web 控制台活动审核日志,请同时启用审核(只读)
  5. 复制并保存客户端 ID客户端密钥
  6. 复制 API 设置页面顶部显示的 API 基准网址。通常为 https://<your-tenant>-services.pm.beyondtrustcloud.com。您将使用此值作为 BPT_API_URL

为 Cloud Run 函数创建服务账号

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

创建服务账号

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

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

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

创建 Pub/Sub 主题

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

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

创建 Cloud Run 函数以收集日志

Cloud Run 函数由来自 Cloud Scheduler 的 Pub/Sub 消息触发,用于从 BeyondTrust EPM API 中提取日志并将其写入 Cloud Storage。

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

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

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

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

  8. 前往安全性标签页:

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

    1. 点击变量和密钥
    2. 为每个环境变量点击+ 添加变量
    变量名称 示例值
    GCS_BUCKET beyondtrust-epm-logs
    GCS_PREFIX beyondtrust-epm/
    STATE_KEY beyondtrust-epm-state.json
    BPT_API_URL https://yourtenant-services.pm.beyondtrustcloud.com
    CLIENT_ID your-client-id
    CLIENT_SECRET your-client-secret
    RECORD_SIZE 1000
    MAX_BATCHES 50
    LOOKBACK_HOURS 24
    • RECORD_SIZE:每个请求的记录数。/Events/FromStartDate 接受 1 到 1000 之间的值;该函数会限制更高的值。
    • MAX_BATCHES:每次运行的请求数量的安全限制。如果运行遇到此标记,光标仍会前进到写入的内容,因此下一次运行会从该位置继续,而不是重复工作。
    • LOOKBACK_HOURS:第一次运行的追溯时间。BeyondTrust 会将被动事件(代码 106、107、603、706)保留 30 天,并将所有其他事件保留 90 天,因此更长的回溯期不会返回比这些时间更早的任何内容。
    • LOOKBACK_HOURS:在没有任何游标的情况下,首次运行要回溯多远。
  10. 变量和 Secret 标签页中,前往请求

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

    • 资源部分中:
      • 内存:选择 512 MiB 或更高值。
      • CPU:选择 1
    • 点击完成
  12. 滚动到执行环境

    • 选择默认(推荐)。
  13. 修订版本伸缩部分中:

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

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

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

添加函数代码

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

    • 第一个文件 - main.py:
    import hashlib
    import json
    import os
    import re
    import time
    import urllib3
    import uuid
    from datetime import datetime, timedelta, timezone
    from urllib.parse import urlencode
    
    import functions_framework
    from google.cloud import storage
    from google.cloud.exceptions import NotFound
    
    # 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()
    
    TOKEN_PATH = '/oauth/connect/token'
    # Events/FromStartDate is the endpoint BeyondTrust documents for bulk extraction:
    # "A new API is exposed to extract the events in bulk." It takes only StartDate and
    # RecordSize. Events/search is not usable here: it requires an OperatingSystem value,
    # it is a single string with no documented "all" value, so it can only ever return one
    # operating system per call.
    EVENTS_PATH = '/management-api/v3/Events/FromStartDate'
    OAUTH_SCOPE = 'urn:management:api'
    # RecordSize accepts 1 to 1000.
    MAX_RECORD_SIZE = 1000
    
    class FetchError(Exception):
        """Raised when the BeyondTrust API call fails.
    
        The cursor must never advance on a failed fetch, otherwise the events in the
        failed window are skipped permanently.
        """
    
    def rfc3339(dt: datetime) -> str:
        """Render a datetime the way StartDate accepts it: milliseconds and a literal Z."""
        return dt.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
    
    def parse_iso(stamp: str) -> datetime:
        """Parse any ISO 8601 timestamp shape the API emits.
    
        event.ingested comes back with a +00:00 offset and up to seven fractional
        digits, e.g. 2026-08-03T13:29:55.1109163+00:00. fromisoformat's tolerance for
        long fractions varies across Python versions, so the fraction is trimmed to
        microseconds first.
        """
        text = stamp.strip()
        if text.endswith('Z'):
            text = text[:-1] + '+00:00'
        text = re.sub(r'\.(\d{1,6})\d*', r'.\1', text, count=1)
        parsed = datetime.fromisoformat(text)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed
    
    def canonical(stamp: str) -> str:
        """Re-render an API timestamp into the one form StartDate accepts.
    
        The API returns event.ingested as +00:00-offset ISO 8601 but rejects that
        same shape as a StartDate value with 400 "Invalid Start date format": it
        accepts only Z-suffixed values. Every timestamp that came from the API must
        pass through here before being sent back or persisted.
        """
        return rfc3339(parse_iso(stamp))
    
    def event_id(evt: dict) -> str:
        """Return the event's identity for deduplication.
    
        Falls back to a content hash when event.id is absent, so an id-less event
        still deduplicates instead of being re-ingested on every boundary re-read.
        """
        explicit = str((evt.get('event') or {}).get('id') or '')
        if explicit:
            return explicit
        digest = hashlib.sha256(
            json.dumps(evt, sort_keys=True, ensure_ascii=False).encode('utf-8')
        ).hexdigest()
        return f'sha256:{digest}'
    
    def event_ingested(evt: dict) -> str:
        """Return the Elastic ingestion timestamp, which is what StartDate filters on."""
        return str((evt.get('event') or {}).get('ingested') or '')
    
    def next_millisecond(stamp: str) -> str:
        """Return the canonical timestamp one millisecond later.
    
        Used only when a full batch fits inside a single ingestion millisecond. Without
        this the cursor cannot move and the collector stalls on that timestamp forever.
        """
        return rfc3339(parse_iso(stamp) + timedelta(milliseconds=1))
    
    @functions_framework.cloud_event
    def main(cloud_event):
        """Fetch BeyondTrust EPM events and write them to Cloud Storage as NDJSON.
    
        Args:
                cloud_event: CloudEvent object containing the Pub/Sub message.
        """
        bucket_name = os.environ.get('GCS_BUCKET')
        prefix = os.environ.get('GCS_PREFIX', 'beyondtrust-epm/')
        state_key = os.environ.get('STATE_KEY', 'beyondtrust-epm-state.json')
    
        api_url = (os.environ.get('BPT_API_URL') or '').rstrip('/')
        client_id = os.environ.get('CLIENT_ID')
        client_secret = os.environ.get('CLIENT_SECRET')
        # Clamp both ends: the API rejects RecordSize outside 1 to 1000, and a
        # misconfigured 0 would otherwise crash-loop on a 400 every run.
        record_size = min(max(int(os.environ.get('RECORD_SIZE', '1000')), 1), MAX_RECORD_SIZE)
        max_batches = int(os.environ.get('MAX_BATCHES', '50'))
        lookback_hours = int(os.environ.get('LOOKBACK_HOURS', '24'))
    
        if not all([bucket_name, api_url, client_id, client_secret]):
            raise RuntimeError(
                'Missing required environment variables: '
                'GCS_BUCKET, BPT_API_URL, CLIENT_ID, CLIENT_SECRET'
            )
    
        bucket = storage_client.bucket(bucket_name)
        state = load_state(bucket, state_key)
    
        # StartDate is a rising cursor on the Elastic ingestion timestamp, not a closed
        # window. There is no EndDate on this endpoint.
    
        if state.get('last_ingested'):
            # canonical() also repairs state written by the previous script revision,
            # which persisted the API's raw +00:00 form that StartDate rejects.
            try:
                start_date = canonical(state['last_ingested'])
            except (ValueError, TypeError, AttributeError) as e:
                raise RuntimeError(
                    f'Unparseable last_ingested in gs://{bucket_name}/{state_key}: '
                    f'{state["last_ingested"]!r}. Fix or delete that object; deleting '
                    f'restarts collection from the lookback window.'
                ) from e
        else:
            start_date = rfc3339(datetime.now(timezone.utc) - timedelta(hours=lookback_hours))
    
        seen_ids = set(state.get('seen_ids', []))
        print(f'Collecting events ingested from {start_date}')
    
        token = get_oauth_token(api_url, client_id, client_secret)
        fresh, cursor, drained = fetch_events(
            api_url, token, start_date, record_size, max_batches, seen_ids
        )
    
        if not fresh:
            print('No new events. Cursor left unchanged.')
            return
    
        # Everything that can fail is computed before the upload: a crash between the
        # upload and save_state replays the batch on the next run, so the window where
        # side effects exist without recorded state must stay minimal. The retained ids
        # share the cursor millisecond, because those are exactly the ones the
        # inclusive StartDate will return again.
        retained = {
            event_id(e) for e in fresh
            if event_ingested(e) and canonical(event_ingested(e)) == cursor
        }
        if cursor == start_date:
            # The cursor millisecond did not advance, so ids retained by earlier runs
            # are still on the boundary; dropping them would re-ingest their events.
            retained |= seen_ids
    
        timestamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
        # The random suffix keeps concurrent executions (Pub/Sub delivers at least
        # once) from overwriting each other's object within the same second.
        filename = (
            f'{prefix}beyondtrust-epm-events-{timestamp}-'
            f'{uuid.uuid4().hex[:8]}.ndjson'
        )
        ndjson = '\n'.join(json.dumps(e, ensure_ascii=False) for e in fresh) + '\n'
        bucket.blob(filename).upload_from_string(
            ndjson, content_type='application/x-ndjson'
        )
        print(f'Wrote {len(fresh)} events to gs://{bucket_name}/{filename}')
    
        save_state(bucket, state_key, {
            'last_ingested': cursor,
            'seen_ids': sorted(retained),
            'updated_at': rfc3339(datetime.now(timezone.utc)),
        })
    
        if not drained:
            print(
                f'Stopped after {max_batches} batches with more events available. '
                'The cursor advanced, so the next run continues from here.'
            )
    
    def fetch_events(api_url, token, start_date, record_size, max_batches, seen_ids):
        """Read forward from start_date until the API returns a short batch.
    
        The response envelope has no page count and no next-page token, so a batch
        shorter than record_size is the only documented end-of-data signal.
    
        StartDate is inclusive and the cursor lands on the newest event's timestamp, so
        every batch re-returns the events sharing it. Deduplication by event id therefore
        has to happen as batches arrive, not only between runs.
    
        Returns:
        Tuple of (new events, cursor to persist, whether the stream was drained).
    
        Raises:
        FetchError: on any API or transport failure, so the caller cannot mistake a
        failed fetch for an empty result and advance the cursor.
        """
        headers = {
            'Authorization': f'Bearer {token}',
            'Accept': 'application/json',
        }
    
        all_events = []
        seen = set(seen_ids)
        cursor = start_date
    
        for batch in range(1, max_batches + 1):
            query = urlencode({'StartDate': cursor, 'RecordSize': record_size})
            body = request_with_retry(f'{api_url}{EVENTS_PATH}?{query}', headers)
    
            events = body.get('events') or []
            new = [e for e in events if event_id(e) not in seen]
            seen.update(event_id(e) for e in events if event_id(e))
            all_events.extend(new)
            print(f'Batch {batch}: {len(events)} events, {len(new)} new')
    
            # Termination is judged on what the API returned, not on what survived
            # deduplication: a batch can be full and still be entirely duplicates.
            # The newest stamp in the batch, not events[-1]: ordering is a client-side
            # convention, not a documented guarantee, and a misordered tail would
            # regress the cursor and re-ingest events already written.
            stamps = [canonical(s) for s in (event_ingested(e) for e in events) if s]
    
            if len(events) < record_size:
                # Short batch: the stream is drained.
                if events and not stamps:
                    print(
                        'Warning: no event in the final batch carries event.ingested; '
                        'cursor left unchanged.'
                    )
                if stamps:
                    cursor = max(max(stamps), cursor)
                return all_events, cursor, True
    
            if not stamps:
                raise FetchError(
                    'No event in a full batch carries event.ingested, '
                    'so the cursor cannot advance'
                )
            batch_max = max(stamps)
            if batch_max <= cursor:
                # A full batch fits inside one ingestion millisecond. Stepping past it is the only
                # way to make progress; holding the cursor here stalls collection forever.
                # Events beyond record_size at that exact millisecond are unreachable, which
                # needs more than 1000 events in one millisecond.
                print(
                    f'Warning: a full batch shares ingestion timestamp {batch_max}; '
                    'stepping past it. Events beyond RecordSize at that timestamp are skipped.'
                )
                cursor = next_millisecond(cursor)
            else:
                cursor = batch_max
    
        return all_events, cursor, False
    
    def request_with_retry(url, headers, attempts=4):
        """GET with backoff on 429 and 5xx.
    
        The documented rate limit is 1000 requests per 100 seconds.
        """
        backoff = 1.0
        for attempt in range(1, attempts + 1):
            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 in (429, 500, 502, 503, 504) and attempt < attempts:
                retry_after = response.headers.get('Retry-After')
                try:
                    delay = int(retry_after) if retry_after else backoff
                except (TypeError, ValueError):
                    delay = backoff
                # Retry-After is server-controlled input: bound it so a bogus value
                # cannot sleep past the function timeout or crash time.sleep.
                delay = min(max(delay, 1.0), 60.0)
                print(f'HTTP {response.status}. Retrying in {delay}s...')
                time.sleep(delay)
                backoff = min(backoff * 2, 30.0)
                continue
    
            if response.status != 200:
                raise FetchError(
                    f'Request failed: {response.status} {response.data.decode("utf-8")}'
                )
    
            text = response.data.decode('utf-8')
            # BeyondTrust's sample folds "Owner" into "owner" before parsing, but that
            # workaround exists for PowerShell's case-insensitive ConvertFrom-Json.
            # Python parses case-sensitively, and the SecOps parser maps file.Owner.*
            # and file.owner to different UDM fields, so both keys must survive.
            try:
                return json.loads(text)
            except json.JSONDecodeError as e:
                raise FetchError(f'Malformed JSON response: {e}') from e
    
        raise FetchError(f'Giving up on {url} after {attempts} attempts')
    
    def get_oauth_token(api_url, client_id, client_secret):
        """Get an access token using the OAuth client credentials flow.
    
        The token is valid for one hour.
        """
        body = urlencode({
            'grant_type': 'client_credentials',
            'client_id': client_id,
            'client_secret': client_secret,
            'scope': OAUTH_SCOPE,
        })
        response = http.request(
            'POST',
            f'{api_url}{TOKEN_PATH}',
            body=body,
            headers={'Content-Type': 'application/x-www-form-urlencoded'},
        )
        if response.status != 200:
            raise FetchError(
                f'Token request failed: {response.status} '
                f'{response.data.decode("utf-8")}'
            )
        return json.loads(response.data.decode('utf-8'))['access_token']
    
    def load_state(bucket, key):
        """Read the collector state from Cloud Storage.
    
        Only a missing object is a cold start. Any other error is raised: swallowing it
        would reset collection to the lookback window and re-ingest that 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):
        """Write the collector state to Cloud Storage.
    
        Failures are raised, not logged. A run that cannot record its cursor must fail,
        otherwise the next run repeats the same window.
        """
        bucket.blob(key).upload_from_string(
            json.dumps(state, indent=2), content_type='application/json'
        )
        print(f"Saved state: last_ingested={state.get('last_ingested')}")
    

    • 第二个文件: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. 在 Google Cloud 控制台中,前往 Cloud Scheduler
  2. 点击创建作业
  3. 提供以下配置详细信息:

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

时间表频率选项

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

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

测试调度器作业

  1. Cloud Scheduler 控制台中,找到您的作业。
  2. 点击强制运行以手动触发。
  3. 等待几秒钟,然后前往 Cloud Run > 服务 > beyondtrust-epm-collector > 日志
  4. 验证函数是否已成功执行。
  5. 检查 Cloud Storage 存储桶,确认日志是否已写入。

检索 Google SecOps 服务账号

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

获取服务账号电子邮件地址

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

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

向 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. 点击保存

在 Google SecOps 中配置 Feed 以注入 BeyondTrust EPM 日志

  1. 依次前往 SIEM 设置 > Feed
  2. 点击添加新 Feed
  3. 点击配置单个 Feed
  4. Feed 名称字段中,输入 Feed 的名称(例如 BeyondTrust EPM logs)。
  5. 选择 Google Cloud Storage V2 作为来源类型
  6. 选择 BeyondTrust Endpoint Privilege Management 作为日志类型
  7. 点击下一步
  8. 为以下输入参数指定值:

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

      gs://beyondtrust-epm-logs/beyondtrust-epm/
      
      • 替换

        • beyondtrust-epm-logs:您的 Cloud Storage 存储桶名称。
        • beyondtrust-epm/:存储日志的可选前缀/文件夹路径(留空表示根目录)。
      • 示例

        • 根存储桶:gs://beyondtrust-epm-logs/
        • 带前缀:gs://beyondtrust-epm-logs/beyondtrust-epm/
    • 来源删除选项:根据您的偏好选择删除选项:

      • 永不删除文件:永不从源中删除文件。建议用于测试目的。
      • 删除已转移的文件和空目录:在成功提取完成后,从来源中删除文件和空目录。

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

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

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

  9. 点击下一步

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

UDM 映射表

日志字段 UDM 映射 逻辑
DomainIdentifier_label additional.fields 已合并
WatsonEventType_label additional.fields 已合并
action_label additional.fields 已合并
activity_id_label additional.fields 已合并
app_id_label additional.fields 已合并
app_insights_instrumentation_key_label additional.fields 已合并
asimov_instrumentation_key_label additional.fields 已合并
authorization_request_control_authorization_label additional.fields 已合并
bundle_name_label additional.fields 已合并
changedBy_label additional.fields 已合并
code_signature_subject_name_label additional.fields 已合并
collector_api_key_label additional.fields 已合并
command_label additional.fields 已合并
configuration_application_group_description_label additional.fields 已合并
configuration_application_group_identifier_label additional.fields 已合并
configuration_application_group_name_label additional.fields 已合并
configuration_application_identifier_label additional.fields 已合并
configuration_application_type_label additional.fields 已合并
configuration_identifier_label additional.fields 已合并
configuration_message_identifier_label additional.fields 已合并
configuration_message_name_label additional.fields 已合并
configuration_message_type_label additional.fields 已合并
configuration_name_label additional.fields 已合并
configuration_revision_number_label additional.fields 已合并
configuration_rule_identifier_label additional.fields 已合并
configuration_rule_on_demand_label additional.fields 已合并
configuration_rule_script_outcome_rule_affected_label additional.fields 已合并
configuration_token_identifier_label additional.fields 已合并
configuration_token_name_label additional.fields 已合并
configuration_workstyle_description_label additional.fields 已合并
configuration_workstyle_identifier_label additional.fields 已合并
configuration_workstyle_name_label additional.fields 已合并
content_length_label additional.fields 已合并
content_type_label additional.fields 已合并
domainNetBIOSName_label additional.fields 已合并
domain_label additional.fields 已合并
entity_label additional.fields 已合并
entity_name_label additional.fields 已合并
event_action_label additional.fields 已合并
exitstatus_label additional.fields 已合并
file_hash_md5_label additional.fields 已合并
file_hash_sha1_label additional.fields 已合并
file_hash_sha256_label additional.fields 已合并
file_version_label additional.fields 已合并
gid_label additional.fields 已合并
group_data_id_label additional.fields 已合并
group_id_label additional.fields 已合并
group_label additional.fields 已合并
handle_label additional.fields 已合并
host_name_label additional.fields 已合并
host_uptime_labels additional.fields 已合并
http_host_label additional.fields 已合并
id_label additional.fields 已合并
iolog_label additional.fields 已合并
is_opted_in_label additional.fields 已合并
linenum_label additional.fields 已合并
local_identifier_label additional.fields 已合并
locale_label additional.fields 已合并
master_utcoffset_label additional.fields 已合并
masterlocale_label additional.fields 已合并
owner_identifier_label additional.fields 已合并
parent_entity_id_label additional.fields 已合并
parent_process_exec_label additional.fields 已合并
parent_process_label additional.fields 已合并
pbmasterdnodename_label additional.fields 已合并
pipeName_label additional.fields 已合并
process_entity_id_label additional.fields 已合并
process_hash_label additional.fields 已合并
process_name_label additional.fields 已合并
process_parent_name_label additional.fields 已合并
process_start_time_label additional.fields 已合并
processexe_label additional.fields 已合并
product_label additional.fields 已合并
product_type_label additional.fields 已合并
product_version_label additional.fields 已合并
requestuser_label additional.fields 已合并
runargv_label additional.fields 已合并
runcwd_label additional.fields 已合并
runeffectivegroup_label additional.fields 已合并
runeffectiveuser_label additional.fields 已合并
runhost_label additional.fields 已合并
schema_version_label additional.fields 已合并
sku_name_label additional.fields 已合并
telemetry_level_label additional.fields 已合并
tenant_id_label additional.fields 已合并
type_label additional.fields 已合并
uid_label additional.fields 已合并
user_id_label additional.fields 已合并
user_name_label additional.fields 已合并
vs_exe_version_label additional.fields 已合并
inter_host intermediary.hostname 直接映射
Processes.description metadata.description 直接映射
details metadata.description 直接映射
event_data.reason metadata.description 直接映射
file.pe.description metadata.description 直接映射
created metadata.event_timestamp 解析为 ISO8601
datetime metadata.event_timestamp 解析为 MMM dd HH:mm:ss
has_principal metadata.event_type 已映射:trueSTATUS_UPDATE
has_user metadata.event_type 已映射:trueUSER_UNCATEGORIZED
parent_working_directory_label metadata.ingestion_labels 已合并
working_directory_label metadata.ingestion_labels 已合并
auditType metadata.product_event_type 直接映射
event_datas.ActionId metadata.product_log_id 直接映射
labels.related_item_id metadata.product_log_id 直接映射
uniqueid metadata.product_log_id 直接映射
masterdversion metadata.product_version 直接映射
headers.http_version network.application_protocol_version 直接映射
headers.request_method network.http.method 直接映射
host.os.platform principal.administrative_domain 直接映射
Processes.process principal.application 直接映射
agent_ephemeral_id_label principal.asset.attribute.labels 已合并
agent_id_label principal.asset.attribute.labels 已合并
agent_version_label principal.asset.attribute.labels 已合并
ecs_version_label principal.asset.attribute.labels 已合并
_hardware principal.asset.hardware 已合并
host.hostname principal.asset.hostname 直接映射
submithost principal.asset.hostname 直接映射
ip_address principal.asset.ip 已合并
masterhostip principal.asset.ip 已合并
submithostip principal.asset.ip 已合并
file.path principal.file.full_path 直接映射
lineinfile principal.file.full_path 直接映射
host.hostname principal.hostname 直接映射
submithost principal.hostname 直接映射
ip_address principal.ip 已合并
masterhostip principal.ip 已合并
submithostip principal.ip 已合并
mac principal.mac 已合并
host.os.name principal.platform_version 直接映射
host.os.version principal.platform_version 直接映射
process.command_line principal.process.command_line 直接映射
runcommand principal.process.command_line 直接映射
process.executable principal.process.file.full_path 直接映射
cmd principal.process.parent_process.command_line 直接映射
Processes.process_path principal.process.parent_process.file.full_path 直接映射
process.parent.executable principal.process.parent_process.file.full_path 直接映射
Processes.parent_process_id principal.process.parent_process.pid 直接映射
Processes.process_id principal.process.pid 直接映射
logpid principal.process.pid 直接映射
file.Owner.DomainName principal.user.company_name 直接映射
file.Owner.Name principal.user.user_display_name 直接映射
runuser principal.user.user_display_name 直接映射
userName principal.user.user_display_name 直接映射
Processes.user principal.user.userid 直接映射
userId principal.user.userid 直接映射
userid principal.user.userid 直接映射
Processes.user_id principal.user.windows_sid 直接映射
EPMWinMac.Configuration.Rule.Action security_result.action 已合并
security_result_action security_result.action 已合并
event_data.outcome security_result.category_details 已合并
host.os.version security_result.category_details 已合并
EPMWinMac.Configuration.Application.Description security_result.description 直接映射
EPMWinMac.Configuration.Message.Description security_result.description 直接映射
host.os.type src.administrative_domain 直接映射
file.name src.file.names 已合并
host.os.full src.platform_version 直接映射
host.os.family target.administrative_domain 直接映射
Processes.dest target.asset.hostname 直接映射
file.extension target.file.mime_type 直接映射
Processes.dest target.hostname 直接映射
host.domain target.hostname 直接映射
file_DriveType_label target.resource.attribute.labels 已合并
file_drive_letter_label target.resource.attribute.labels 已合并
owner_label target.resource.attribute.labels 已合并
不适用 metadata.event_type 常量:USER_UNCATEGORIZED
不适用 metadata.product_name 常量:Beyondtrust Privilege Management
不适用 metadata.vendor_name 常量:Beyondtrust Privilege Management
不适用 network.application_protocol 常量:HTTP
不适用 principal.platform 常量:MAC

更新日志

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

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