收集 Nagios XI 日志
本文档介绍了如何使用 Google Cloud Storage V2 将 Nagios XI 日志注入到 Google Security Operations。
Nagios XI 是一款全面的基础架构监控解决方案,可监控服务器、网络、应用和服务。它通过 REST API 跟踪主机和服务状态、性能指标,并针对 IT 基础架构问题生成提醒。
准备工作
确保您满足以下前提条件:
- Google SecOps 实例
- 启用了 Cloud Storage、Cloud Run、Pub/Sub 和 Cloud Scheduler API 的 GCP 项目
- 创建和管理 GCS 存储分区的权限
- 管理 GCS 存储分区的 IAM 政策的权限
- 创建 Cloud Run 服务、Pub/Sub 主题和 Cloud Scheduler 作业的权限
- 对 Nagios XI 的特权访问权限(具有用户管理权限)
- Nagios XI 版本 5 或更高版本(支持 REST API)
配置 Nagios XI API 访问权限
如需让 Google SecOps 检索监控数据,您需要创建一个具有 API 访问权限和只读权限的用户账号。
创建拥有只读权限的用户并授予 API 访问权限
- 以管理员权限登录 Nagios XI 网页界面。
- 依次前往管理 > 管理用户。
- 点击添加新用户。
- 提供以下配置详细信息:
- 用户名:输入一个描述性名称(例如
chronicle-integration) - 密码:输入安全密码
- 名称:输入
Chronicle Integration User - 电子邮件地址:输入有效的电子邮件地址
- 用户名:输入一个描述性名称(例如
- 在安全设置部分中,配置以下各项:
- 授权级别:选择用户
- 可查看所有主机和服务:选中此选项
- 只读权限:选中此选项
- API 访问权限:选中此选项
- 取消选中以下选项:
- 强制用户在下次登录时更改密码
- 通过电子邮件发送用户账号信息
- 创建为监控联系人
- 确保选中账号已启用。
- 点击添加用户。
检索 API 密钥
- 在管理用户页面中,点击相应用户账号 (
chronicle-integration)。 - 在用户账号设置页面中,找到 API 密钥字段。
复制 API 密钥值。
验证权限
如需验证账号是否具有所需权限,请执行以下操作:
- 使用
chronicle-integration用户账号登录 Nagios XI。 - 依次前往首页 > 主机状态。
- 如果您能看到所有受监控的主机和服务,则表示您拥有所需的权限。
- 如果您看不到主机或服务,请与管理员联系,以获取可查看所有主机和服务权限。
测试 API 访问权限
在继续进行集成之前,请先测试您的凭据:
# Replace with your actual values NAGIOS_HOST="https://your-nagios-server.example.com" API_KEY="your-api-key" # Test API access - query host status curl -k "${NAGIOS_HOST}/nagiosxi/api/v1/objects/hoststatus?apikey=${API_KEY}&pretty=1"如果响应成功,则会返回一个包含主机状态信息的 JSON 对象。
创建 Google Cloud Storage 存储桶
- 前往 Google Cloud 控制台。
- 选择您的项目或创建新项目。
- 在导航菜单中,依次前往 Cloud Storage > 存储分区。
- 点击创建存储分区。
提供以下配置详细信息:
设置 值 为存储桶命名 输入一个全局唯一的名称(例如 nagios-xi-logs)位置类型 根据您的需求进行选择(区域级、双区域、多区域) 位置 选择营业地点(例如 us-central1)存储类别 标准(建议用于经常访问的日志) 访问权限控制 均匀(推荐) 保护工具 可选:启用对象版本控制或保留政策 点击创建。
为 Cloud Run 函数创建服务账号
- 在 GCP 控制台中,依次前往 IAM 和管理 > 服务账号。
- 点击创建服务账号。
- 提供以下配置详细信息:
- 服务账号名称:输入
nagios-logs-collector-sa - 服务账号说明:输入
Service account for Cloud Run function to collect Nagios XI logs
- 服务账号名称:输入
- 点击创建并继续。
- 在向此服务账号授予对项目的访问权限部分中,添加以下角色:
- 点击选择角色。
- 搜索并选择 Storage Object Admin。
- 点击 + 添加其他角色。
- 搜索并选择 Cloud Run Invoker。
- 点击 + 添加其他角色。
- 搜索并选择 Cloud Functions Invoker。
- 点击继续。
- 点击完成。
授予对 GCS 存储桶的 IAM 权限
- 前往 Cloud Storage > 存储分区。
- 点击您的存储桶名称 (
nagios-xi-logs)。 - 前往权限标签页。
- 点击授予访问权限。
- 提供以下配置详细信息:
- 添加主账号:输入服务账号电子邮件地址 (
nagios-logs-collector-sa@PROJECT_ID.iam.gserviceaccount.com) - 分配角色:选择 Storage Object Admin
- 添加主账号:输入服务账号电子邮件地址 (
- 点击保存。
创建 Pub/Sub 主题
- 在 GCP 控制台中,前往 Pub/Sub > 主题。
- 点击创建主题。
- 提供以下配置详细信息:
- 主题 ID:输入
nagios-logs-trigger - 将其他设置保留为默认值
- 主题 ID:输入
- 点击创建。
创建 Cloud Run 函数以收集日志
Cloud Run 函数将由来自 Cloud Scheduler 的 Pub/Sub 消息触发,以从 Nagios XI REST API 中提取日志并将其写入 GCS。
- 在 GCP 控制台中,前往 Cloud Run。
- 点击创建服务。
- 选择函数(使用内嵌编辑器创建函数)。
在配置部分中,提供以下配置详细信息:
设置 值 Service 名称 nagios-logs-collector区域 选择与您的 GCS 存储桶匹配的区域(例如 us-central1)运行时 选择 Python 3.12 或更高版本 在触发器(可选)部分中:
- 点击 + 添加触发器。
- 选择 Cloud Pub/Sub。
- 在选择 Cloud Pub/Sub 主题中,选择
nagios-logs-trigger。 - 点击保存。
在身份验证部分中:
- 选择需要进行身份验证。
- 检查 Identity and Access Management (IAM)。
向下滚动并展开容器、网络、安全性。
前往安全性标签页:
- 服务账号:选择
nagios-logs-collector-sa
- 服务账号:选择
前往容器标签页:
- 点击变量和密钥。
- 为每个环境变量点击+ 添加变量:
变量名称 示例值 说明 GCS_BUCKETnagios-xi-logsGCS 存储桶名称 GCS_PREFIXnagios-xi日志文件的前缀 STATE_KEYnagios-xi/state.json状态文件路径 NAGIOS_BASE_URLhttps://your-nagios-server.example.comNagios XI 基本网址 NAGIOS_API_KEYyour-api-keyNagios XI API 密钥 MAX_RECORDS1000每次运行中每个端点的记录数上限 PAGE_SIZE200每个 API 页面的记录数 LOOKBACK_HOURS24初始回溯期 在变量和 Secret 部分中,向下滚动到请求:
- 请求超时:输入
600秒(10 分钟)
- 请求超时:输入
前往设置标签页:
- 在资源部分中:
- 内存:选择 512 MiB 或更高值
- CPU:选择 1
- 在资源部分中:
在修订版本伸缩部分中:
- 实例数下限:输入
0 - 实例数上限:输入
100
- 实例数下限:输入
点击创建。
等待服务创建完成(1-2 分钟)。
创建服务后,系统会自动打开内嵌代码编辑器。
添加函数代码
- 在入口点字段中输入 main。
在内嵌代码编辑器中,创建两个文件:
main.py:
import functions_framework from google.cloud import storage import json import os import urllib3 from datetime import datetime, timezone, timedelta import time http = urllib3.PoolManager( timeout=urllib3.Timeout(connect=5.0, read=30.0), retries=False, ) storage_client = storage.Client() GCS_BUCKET = os.environ.get('GCS_BUCKET') GCS_PREFIX = os.environ.get('GCS_PREFIX', 'nagios-xi') STATE_KEY = os.environ.get('STATE_KEY', 'nagios-xi/state.json') NAGIOS_BASE_URL = os.environ.get('NAGIOS_BASE_URL', '').rstrip('/') NAGIOS_API_KEY = os.environ.get('NAGIOS_API_KEY') MAX_RECORDS = int(os.environ.get('MAX_RECORDS', '1000')) PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '200')) LOOKBACK_HOURS = int(os.environ.get('LOOKBACK_HOURS', '24')) ENDPOINTS = [ 'hoststatus', 'servicestatus', 'statehistory', 'logentries', ] @functions_framework.cloud_event def main(cloud_event): if not all([GCS_BUCKET, NAGIOS_BASE_URL, NAGIOS_API_KEY]): 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()}") start_ts = int(last_time.timestamp()) end_ts = int(now.timestamp()) all_records = [] newest_time = None for endpoint in ENDPOINTS: records, endpoint_newest = fetch_nagios_endpoint( endpoint, start_ts, end_ts ) for record in records: record['_nagios_endpoint'] = endpoint all_records.extend(records) if endpoint_newest: if newest_time is None or endpoint_newest > newest_time: newest_time = endpoint_newest 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}/nagios_logs_{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}") if newest_time: save_state(bucket, newest_time) else: save_state(bucket, now.isoformat()) print(f"Successfully processed {len(all_records)} records") except Exception as e: print(f'Error processing logs: {str(e)}') raise def fetch_nagios_endpoint(endpoint, start_ts, end_ts): base_url = f"{NAGIOS_BASE_URL}/nagiosxi/api/v1/objects/{endpoint}" records = [] newest_time = None start_index = 0 page_num = 0 backoff = 1.0 while True: page_num += 1 if len(records) >= MAX_RECORDS: print(f"{endpoint}: Reached max_records limit ({MAX_RECORDS})") break remaining = min(PAGE_SIZE, MAX_RECORDS - len(records)) params = [ f"apikey={NAGIOS_API_KEY}", f"starttime={start_ts}", f"endtime={end_ts}", f"records={remaining}", f"start={start_index}", ] url = f"{base_url}?{'&'.join(params)}" try: response = http.request('GET', url) if response.status == 429: retry_after = int(response.headers.get('Retry-After', str(int(backoff)))) print(f"{endpoint}: Rate limited (429). Retrying after {retry_after}s...") time.sleep(retry_after) backoff = min(backoff * 2, 30.0) continue backoff = 1.0 if response.status != 200: print(f"{endpoint}: HTTP Error {response.status}") response_text = response.data.decode('utf-8') print(f"Response body: {response_text}") break data = json.loads(response.data.decode('utf-8')) record_count = data.get('recordcount', 0) result_key = get_result_key(endpoint) page_results = data.get(result_key, []) if not page_results: print(f"{endpoint}: No more results at offset {start_index}") break print(f"{endpoint} page {page_num}: Retrieved {len(page_results)} events") records.extend(page_results) for event in page_results: try: event_time = event.get('state_time') or event.get('status_update_time') or event.get('entry_time') 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}") if len(page_results) < remaining: print(f"{endpoint}: Reached last page (returned {len(page_results)} < {remaining})") break start_index += len(page_results) except Exception as e: print(f"{endpoint}: Error fetching logs: {e}") break print(f"{endpoint}: Retrieved {len(records)} total records from {page_num} pages") return records, newest_time def get_result_key(endpoint): key_map = { 'hoststatus': 'hoststatus', 'servicestatus': 'servicestatus', 'statehistory': 'statehistory', 'logentries': 'logentry', } return key_map.get(endpoint, endpoint) 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
点击部署以保存并部署该函数。
等待部署完成(2-3 分钟)。
创建 Cloud Scheduler 作业
- 在 GCP Console 中,前往 Cloud Scheduler。
- 点击创建作业。
提供以下配置详细信息:
设置 值 名称 nagios-logs-collector-hourly区域 选择与 Cloud Run 函数相同的区域 频率 0 * * * *(每小时一次,整点时)时区 选择时区(建议选择世界协调时间 [UTC]) 目标类型 Pub/Sub 主题 选择 nagios-logs-trigger消息正文 {}(空 JSON 对象)点击创建。
时间表频率选项
根据日志量和延迟时间要求选择频次:
| 频率 | Cron 表达式 | 使用场景 |
|---|---|---|
| 每隔 5 分钟 | */5 * * * * |
大批量、低延迟 |
| 每隔 15 分钟 | */15 * * * * |
搜索量中等 |
| 每小时 | 0 * * * * |
标准(推荐) |
| 每 6 小时 | 0 */6 * * * |
低成交量、批处理 |
| 每天 | 0 0 * * * |
历史数据收集 |
测试集成
- 在 Cloud Scheduler 控制台中,找到您的作业 (
nagios-logs-collector-hourly)。 - 点击强制运行以手动触发作业。
- 等待几秒钟。
- 前往 Cloud Run > 服务。
- 点击
nagios-logs-collector。 - 点击日志标签页。
验证函数是否已成功执行。请查找以下内容:
Fetching logs from YYYY-MM-DDTHH:MM:SS+00:00 to YYYY-MM-DDTHH:MM:SS+00:00 hoststatus page 1: Retrieved X events servicestatus page 1: Retrieved X events statehistory page 1: Retrieved X events logentries page 1: Retrieved X events Wrote X records to gs://nagios-xi-logs/nagios-xi/nagios_logs_YYYYMMDD_HHMMSS.ndjson Successfully processed X records前往 Cloud Storage > 存储分区。
点击
nagios-xi-logs。转到
nagios-xi/文件夹。验证是否已创建具有当前时间戳的新
.ndjson文件。
如果您在日志中看到错误,请执行以下操作:
- HTTP 401:验证
NAGIOS_API_KEY环境变量是否正确,以及用户是否已启用 API 访问权限 - HTTP 403:验证用户账号是否具有可查看所有主机和服务权限
- HTTP 429:速率限制 - 函数将自动重试并进行退避
- 缺少环境变量:验证是否已在 Cloud Run 函数配置中设置所有必需的变量
检索 Google SecOps 服务账号
- 依次前往 SIEM 设置 > Feed。
- 点击添加新 Feed。
- 点击配置单个 Feed。
- 在 Feed 名称字段中,输入 Feed 的名称(例如
Nagios XI Logs GCS)。 - 选择 Google Cloud Storage V2 作为来源类型。
- 选择 Nagios 作为日志类型。
- 点击获取服务账号。
系统会显示一个唯一的服务账号电子邮件地址。例如:
chronicle-12345678@chronicle-gcp-prod.iam.gserviceaccount.com复制此电子邮件地址,以便在下一步中使用。
点击下一步。
为以下输入参数指定值:
存储桶网址:输入带有前缀路径的 GCS 存储桶 URI:
gs://nagios-xi-logs/nagios-xi/
来源删除选项:根据您的偏好选择删除选项:
- 永不:转移后永不删除任何文件(建议用于测试)。
- 删除已转移的文件:在成功转移后删除文件。
删除已转移的文件和空目录:成功转移后删除文件和空目录。
文件存在时间上限:包含在过去指定天数内修改的文件(默认值为 180 天)
资产命名空间:资产命名空间
注入标签:要应用于此 Feed 中事件的标签
点击下一步。
在最终确定界面中查看新的 Feed 配置,然后点击提交。
向 Google SecOps 服务账号授予 IAM 权限
- 前往 Cloud Storage > 存储分区。
- 点击
nagios-xi-logs。 - 前往权限标签页。
- 点击授予访问权限。
- 提供以下配置详细信息:
- 添加主账号:粘贴 Google SecOps 服务账号电子邮件地址
- 分配角色:选择 Storage Object Viewer
点击保存。
UDM 映射表
| 日志字段 | UDM 映射 | 逻辑 |
|---|---|---|
| src_ip | has_principal | 如果成功将 src_ip 合并到 principal.ip 和 principal.asset.ip,则设置为“true” |
| src_ip | principal.asset.ip | 直接复制值 |
| src_ip | principal.ip | 直接复制值 |
| jobid | jobid_label.key | 设置为“Jobid” |
| jobid | jobid_label.value | 直接复制值 |
| jobid_label | principal.resource.attribute.labels | 已从 jobid_label 合并 |
| pid | principal.process.pid | 直接复制值 |
| ent | metadata.product_event_type | 直接复制值 |
| 说明 | metadata.description | 直接复制值 |
| msg_ip | has_target | 如果成功将 msg_ip 合并到 target.ip 和 target.asset.ip,则设置为“true” |
| msg_ip | target.asset.ip | 直接复制值 |
| msg_ip | target.ip | 直接复制值 |
| 端口 | target.port | 直接复制并转换为整数的值 |
| 第 1 列 | principal.asset.hostname | 如果 ent == "SERVICE ALERT",则为 column1 中的值;如果 ent == "SERVICE NOTIFICATION",则为 column2 中的值 |
| 第 1 列 | principal.hostname | 如果 ent == "SERVICE ALERT",则为 column1 中的值;如果 ent == "SERVICE NOTIFICATION",则为 column2 中的值 |
| 第 1 列 | principal.user.user_display_name | 如果 ent == "SERVICE NOTIFICATION",则直接复制值 |
| 第 2 列 | security_result.summary | 如果 ent == "SERVICE ALERT" 且 column2 不为空,则为 column3 中的值;否则,如果 ent == "SERVICE NOTIFICATION",则为 column3 中的值 |
| 第 3 列 | security_result.summary | 如果 ent == "SERVICE ALERT" 且 column3 不为空,则为 column3 中的值;否则,如果 ent == "SERVICE NOTIFICATION",则为 column3 中的值 |
| column4 | security_result.severity | 如果 column4 在 ["LOW", "MEDIUM", "HIGH", "CRITICAL"] 中,且 ent 在 ["SERVICE NOTIFICATION", "SERVICE ALERT"] 中,则直接复制值 |
| column4 | security_result.severity_details | 如果 column4 不在 ["LOW", "MEDIUM", "HIGH", "CRITICAL"] 中,且 ent 在 ["SERVICE NOTIFICATION", "SERVICE ALERT"] 中,则直接复制值 |
| has_principal | metadata.event_type | 如果 has_principal 和 has_target 为 true,则设置为“NETWORK_CONNECTION”;否则,如果 has_principal 为 true,则设置为“STATUS_UPDATE”;否则,如果 has_principal_user 为 true,则设置为“USER_UNCATEGORIZED”;否则,设置为“GENERIC_EVENT” |
| has_target | metadata.event_type | |
| has_principal_user | metadata.event_type | |
| security_result | event.idm.read_only_udm.security_result | 从 security_result 合并 |
| 元数据 | event.idm.read_only_udm.metadata | 已从元数据重命名 |
| 目标 | event.idm.read_only_udm.target | 已从目标重命名 |
| 主账号 | event.idm.read_only_udm.principal | 已重命名为“principal” |
| 网络 | event.idm.read_only_udm.network | 从网络重命名 |
| metadata.product_name | metadata.product_name | 设置为“NAGIOS” |
| metadata.vendor_name | metadata.vendor_name | 设置为“NAGIOS” |