收集 LenelS2 OnGuard 日志
本文档介绍了如何从 LenelS2 (Honeywell) OnGuard 收集日志,并使用 Webhook Feed 将其转发到 Google Security Operations。由于 OnGuard 本身不支持直接 webhook 或 HTTP POST 传送,因此本指南使用自定义中间件脚本来轮询 OnGuard OpenAccess REST API 以获取事件,并将这些事件转发到 Google SecOps webhook 提取端点。
LenelS2 OnGuard 是一套实体访问权限控制和徽章管理系统,可管理企业设施中的持卡人、凭据、读卡器、面板和门禁事件。OpenAccess REST API 可提供对 OnGuard 数据的程序化访问,包括已记录的事件,例如访问权限授予、访问权限拒绝、门保持打开、门被强行打开和警报事件。
准备工作
请确保满足以下前提条件:
- Google SecOps 实例
- 已激活 OpenAccess 许可的 OnGuard 服务器(版本 7.5 或更高版本)
在 OpenAccess 服务器上运行的以下 OnGuard 服务:
- LS Communication Server
- LS 事件上下文提供程序
- LS Web Event Bridge
- LS OpenAccess
- LS Web 服务
具有以下最低权限的内部 OnGuard 用户账号:
- 系统权限:在访问权限控制硬件下,对门禁面板、读卡器和警报面板的查询权限
- 系统权限:在访问权限控制下,对细分拥有查询权限
- 监控权限:在控制下,开门和继电器和读卡器输出
从中间件主机到 OnGuard 服务器(通过 TCP 端口
8080,即默认的 OpenAccess 端口)的网络连接从中间件主机到互联网的网络连接(用于访问 Google SecOps webhook 端点)
中间件主机上安装了 Python 3.8 或更高版本
对 Google Cloud Console 的访问权限(用于创建 API 密钥)
在 Google SecOps 中创建 Webhook Feed
创建 Feed
- 依次前往 SIEM 设置 > Feed。
- 点击添加新 Feed。
- 在下一页上,点击配置单个 Feed。
- 在 Feed 名称字段中,输入 Feed 的名称(例如
LenelS2 OnGuard Events)。 - 选择 Webhook 作为来源类型。
- 选择 Lenel OnGuard 作为日志类型。
- 点击下一步。
- 为以下输入参数指定值:
- 拆分定界符:输入
\n(换行符定界符,因为中间件以 NDJSON 格式发送事件) - 资产命名空间:资产命名空间
- 注入标签:要应用于此 Feed 中事件的标签
- 拆分定界符:输入
- 点击下一步。
- 在最终确定界面中查看新的 Feed 配置,然后点击提交。
生成并保存密钥
创建 Feed 后,您必须生成用于身份验证的密钥:
- 在 Feed 详情页面上,点击生成密钥。
- 系统会显示一个对话框,其中包含密钥。
- 复制并妥善保存此密钥。
获取 Feed 端点网址
- 前往相应 Feed 的详细信息标签页。
- 在端点信息部分,复制 Feed 端点网址。
网址格式为:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate或
https://<REGION>-malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate保存此网址以供后续步骤使用。
点击完成。
创建 Google Cloud API 密钥
Chronicle 需要 API 密钥才能进行身份验证。在 Google Cloud Console 中创建受限 API 密钥。
创建 API 密钥
- 前往 Google Cloud 控制台的“凭据”页面。
- 选择您的项目(与您的 Chronicle 实例关联的项目)。
- 依次点击创建凭据> API 密钥。
- 系统会创建一个 API 密钥,并在对话框中显示该密钥。
- 点击修改 API 密钥以限制密钥。
限制 API 密钥
- 在 API 密钥设置页面中:
- 名称:输入一个描述性名称(例如
Chronicle Webhook API Key)
- 名称:输入一个描述性名称(例如
- 在 API 限制下:
- 选择限制密钥。
- 在选择 API 下拉菜单中,搜索并选择 Google SecOps API(或 Chronicle API)。
- 点击保存。
- 从页面顶部的 API 密钥字段复制 API 密钥值。
- 安全地保存 API 密钥。
验证 OnGuard OpenAccess 前提条件
在配置中间件之前,请验证 OnGuard OpenAccess API 是否正常运行。
验证 OpenAccess 服务是否正在运行
- 在 OnGuard 服务器上,打开 Windows 服务 (
services.msc)。 - 确认以下服务正在运行:
- LS OpenAccess
- LS Communication Server
- LS 事件上下文提供程序
- LS Web Event Bridge
- LS Web 服务
- LS Message Broker (RabbitMQ)
- 如果任何服务已停止,请右键点击该服务,然后选择启动。
验证 OpenAccess API 是否可访问
- 在中间件主机上打开网络浏览器。
导航到以下网址:
https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess/version?version=1.0将
<ONGUARD_SERVER>替换为 OnGuard 服务器的 IP 地址或主机名。API 会返回一个包含 OpenAccess 版本详细信息的 JSON 响应,以确认该服务可访问。
检索身份验证目录
发送 GET 请求以检索可用的身份验证目录:
https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess/directories?version=1.0响应包含目录列表。记下您将用于身份验证的目录(通常是内部 OnGuard 目录)的
ID和Name。
验证 OnGuard 用户账号
- 在 OnGuard 服务器上打开 OnGuard 系统管理应用。
- 依次前往管理 > 用户。
- 选择指定用于 API 访问的用户账号。
- 点击权限标签页。
- 在系统权限组下,确认已启用以下权限:
- 在访问权限控制硬件下:查询门禁面板、读卡器和警报面板
- 在访问权限控制下:对细分进行查询
- 在监控权限组下,确认已启用以下权限:
- 在控制下:打开车门
- 在控制下:Relay 和阅读器输出
获取应用 ID
使用 OpenAccess API 的每个应用都必须具有唯一的应用 ID。这是您定义的字符串值(例如 ChronicleForwarder)。系统会随每个 API 请求一起发送应用 ID 作为 Application-Id HTTP 标头。
配置 OnGuard-to-Chronicle 中间件
由于 OnGuard 本身不支持出站 webhook 或 HTTP POST 传送,因此需要一个中间件脚本来轮询 OpenAccess REST API 以获取已记录的事件,并将这些事件转发到 Google SecOps webhook 端点。
构建 Chronicle 网络钩子网址
将 Chronicle 端点网址和 API 密钥组合在一起:
<ENDPOINT_URL>?key=<API_KEY>示例:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...
创建中间件脚本
在中间件主机上,为脚本创建一个目录:
mkdir -p /opt/onguard-chronicle-forwarder cd /opt/onguard-chronicle-forwarder安装所需的 Python 软件包:
pip3 install requests创建转发器脚本:
nano onguard_to_chronicle.py将以下内容添加到脚本中:
#!/usr/bin/env python3 """LenelS2 OnGuard to Google SecOps (Chronicle) Event Forwarder. Polls the OnGuard OpenAccess REST API for logged events and forwards them to a Chronicle webhook ingestion endpoint. """ import json import logging import os import sys import time from datetime import datetime, timedelta, timezone import requests import urllib3 # Suppress SSL warnings if using self-signed certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # ── Configuration ────────────────────────────────────────────────── ONGUARD_HOST = os.environ.get("ONGUARD_HOST", "192.168.1.100") ONGUARD_PORT = os.environ.get("ONGUARD_PORT", "8080") ONGUARD_USERNAME = os.environ.get("ONGUARD_USERNAME", "") ONGUARD_PASSWORD = os.environ.get("ONGUARD_PASSWORD", "") ONGUARD_DIRECTORY_ID = os.environ.get("ONGUARD_DIRECTORY_ID", "") APPLICATION_ID = os.environ.get("APPLICATION_ID", "ChronicleForwarder") VERIFY_SSL = os.environ.get("VERIFY_SSL", "false").lower() == "true" CHRONICLE_ENDPOINT = os.environ.get("CHRONICLE_ENDPOINT", "") CHRONICLE_API_KEY = os.environ.get("CHRONICLE_API_KEY", "") CHRONICLE_SECRET_KEY = os.environ.get("CHRONICLE_SECRET_KEY", "") POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "30")) PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "100")) LOOKBACK_MINUTES = int(os.environ.get("LOOKBACK_MINUTES", "5")) # ── Logging ──────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler("/opt/onguard-chronicle-forwarder/forwarder.log"), ], ) logger = logging.getLogger(__name__) BASE_URL = f"https://{ONGUARD_HOST}:{ONGUARD_PORT}/api/access/onguard/openaccess" class OnGuardClient: """Client for the OnGuard OpenAccess REST API.""" def __init__(self): self.session_token = None self.headers = { "Content-Type": "application/json", "Application-Id": APPLICATION_ID, } def authenticate(self): """Authenticate to OpenAccess and obtain a session token.""" url = f"{BASE_URL}/authentication?version=1.0" payload = { "user_name": ONGUARD_USERNAME, "password": ONGUARD_PASSWORD, "directory_id": ONGUARD_DIRECTORY_ID, } try: resp = requests.post( url, headers=self.headers, json=payload, verify=VERIFY_SSL, timeout=30, ) resp.raise_for_status() data = resp.json() self.session_token = data.get("session_token") self.headers["Session-Token"] = self.session_token logger.info("Successfully authenticated to OnGuard OpenAccess.") except requests.exceptions.RequestException as exc: logger.error("Authentication failed: %s", exc) raise def keepalive(self): """Renew the session idle timeout.""" url = f"{BASE_URL}/keepalive?version=1.0" try: resp = requests.get( url, headers=self.headers, verify=VERIFY_SSL, timeout=15 ) resp.raise_for_status() except requests.exceptions.RequestException: logger.warning("Keepalive failed. Re-authenticating.") self.authenticate() def get_logged_events(self, start_time, page_number=1): """Retrieve a page of logged events since start_time.""" url = f"{BASE_URL}/logged_events?version=1.0" params = { "filter": f"timestamp >= '{start_time}'", "page_number": page_number, "page_size": PAGE_SIZE, "order_by": "timestamp", } try: resp = requests.get( url, headers=self.headers, params=params, verify=VERIFY_SSL, timeout=30, ) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as exc: logger.error("Failed to retrieve logged events: %s", exc) return None def logout(self): """Logout and invalidate the session token.""" url = f"{BASE_URL}/authentication?version=1.0" try: requests.delete( url, headers=self.headers, verify=VERIFY_SSL, timeout=15 ) logger.info("Logged out of OnGuard OpenAccess.") except requests.exceptions.RequestException: pass def send_to_chronicle(events): """Send a batch of events to the Chronicle webhook endpoint.""" if not events: return True url = f"{CHRONICLE_ENDPOINT}?key={CHRONICLE_API_KEY}" headers = { "Content-Type": "application/json", "x-chronicle-auth": CHRONICLE_SECRET_KEY, } # Send events as NDJSON (newline-delimited JSON) body = "\n".join(json.dumps(event) for event in events) try: resp = requests.post(url, headers=headers, data=body, timeout=30) resp.raise_for_status() logger.info("Sent %d events to Chronicle.", len(events)) return True except requests.exceptions.RequestException as exc: logger.error("Failed to send events to Chronicle: %s", exc) return False def main(): """Main polling loop.""" logger.info("Starting OnGuard-to-Chronicle forwarder.") client = OnGuardClient() client.authenticate() last_poll_time = datetime.now(timezone.utc) - timedelta(minutes=LOOKBACK_MINUTES) while True: try: current_time = datetime.now(timezone.utc) start_time_str = last_poll_time.strftime("%Y-%m-%dT%H:%M:%SZ") all_events = [] page = 1 while True: result = client.get_logged_events(start_time_str, page_number=page) if result is None: # Session may have expired; re-authenticate client.authenticate() result = client.get_logged_events( start_time_str, page_number=page ) if result is None: break events = result.get("logged_events", []) if not events: break all_events.extend(events) total = result.get("total_items", 0) if page * PAGE_SIZE >= total: break page += 1 if all_events: success = send_to_chronicle(all_events) if success: last_poll_time = current_time else: last_poll_time = current_time logger.debug("No new events found.") # Send keepalive to maintain session client.keepalive() except KeyboardInterrupt: logger.info("Shutting down.") client.logout() sys.exit(0) except Exception as exc: logger.error("Unexpected error: %s", exc) try: client.authenticate() except Exception: pass time.sleep(POLL_INTERVAL_SECONDS) if __name__ == "__main__": main()保存并关闭文件。
配置环境变量
为转发器创建环境文件:
nano /opt/onguard-chronicle-forwarder/.env添加以下配置值:
ONGUARD_HOST=<ONGUARD_SERVER_IP_OR_HOSTNAME> ONGUARD_PORT=8080 ONGUARD_USERNAME=<ONGUARD_API_USERNAME> ONGUARD_PASSWORD=<ONGUARD_API_PASSWORD> ONGUARD_DIRECTORY_ID=<DIRECTORY_ID> APPLICATION_ID=ChronicleForwarder VERIFY_SSL=false CHRONICLE_ENDPOINT=<CHRONICLE_WEBHOOK_ENDPOINT_URL> CHRONICLE_API_KEY=<GOOGLE_CLOUD_API_KEY> CHRONICLE_SECRET_KEY=<CHRONICLE_FEED_SECRET_KEY> POLL_INTERVAL_SECONDS=30 PAGE_SIZE=100 LOOKBACK_MINUTES=5- ONGUARD_HOST:运行 OpenAccess 服务的 OnGuard 服务器的 IP 地址或主机名(例如
192.168.1.100) - ONGUARD_PORT:OpenAccess 服务监听的端口(默认值:
8080) - ONGUARD_USERNAME:OnGuard 内部用户账号的用户名
- ONGUARD_PASSWORD:OnGuard 用户账号的密码
- ONGUARD_DIRECTORY_ID:从
GET /directories端点检索到的目录 ID - APPLICATION_ID:相应集成的唯一标识符(例如
ChronicleForwarder) - VERIFY_SSL:如果 OnGuard 服务器使用受信任的 SSL 证书,则设置为
true;如果使用自签名证书,则设置为false - CHRONICLE_ENDPOINT:来自 Chronicle Feed 配置的 Webhook 端点网址
- CHRONICLE_API_KEY:之前创建的 Google Cloud API 密钥
- CHRONICLE_SECRET_KEY:在创建 Chronicle Feed 期间生成的密钥
- POLL_INTERVAL_SECONDS:轮询新事件的频率(以秒为单位)(默认值:
30) - PAGE_SIZE:每个 API 页面要检索的事件数(默认值:
100,最大值:1000) - LOOKBACK_MINUTES:在初始启动时,要检索多少分钟的历史事件(默认值:
5)
- ONGUARD_HOST:运行 OpenAccess 服务的 OnGuard 服务器的 IP 地址或主机名(例如
保护环境文件:
chmod 600 /opt/onguard-chronicle-forwarder/.env
作为 systemd 服务安装
创建 systemd 服务文件:
sudo nano /etc/systemd/system/onguard-chronicle-forwarder.service添加以下内容:
[Unit] Description=LenelS2 OnGuard to Chronicle Event Forwarder After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/onguard-chronicle-forwarder EnvironmentFile=/opt/onguard-chronicle-forwarder/.env ExecStart=/usr/bin/python3 /opt/onguard-chronicle-forwarder/onguard_to_chronicle.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable onguard-chronicle-forwarder.service sudo systemctl start onguard-chronicle-forwarder.service验证服务是否正在运行:
sudo systemctl status onguard-chronicle-forwarder.service检查转发器日志,确认操作是否成功:
tail -f /opt/onguard-chronicle-forwarder/forwarder.log成功启动后,系统会显示:
Starting OnGuard-to-Chronicle forwarder. Successfully authenticated to OnGuard OpenAccess. Sent <N> events to Chronicle.
OpenAccess API 参考文档
中间件使用以下 OnGuard OpenAccess REST API 端点。所有端点均相对于 https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess。
身份验证
| 方法 | 端点 | 说明 |
|---|---|---|
POST |
/authentication?version=1.0 |
登录并检索会话令牌 |
DELETE |
/authentication?version=1.0 |
退出并使会话令牌失效 |
GET |
/directories?version=1.0 |
获取可用的身份验证目录 |
GET |
/keepalive?version=1.0 |
续订会话空闲超时时间 |
所有经过身份验证的请求所需的 HTTP 标头:
| 标题 | 说明 |
|---|---|
Application-Id |
唯一的应用标识符字符串(例如 ChronicleForwarder) |
Session-Token |
由 POST /authentication 返回的会话令牌 |
Content-Type |
application/json |
事件
| 方法 | 端点 | 说明 |
|---|---|---|
GET |
/logged_events?version=1.0 |
检索记录的事件的某个页面 |
POST |
/event_subscriptions?version=1.0 |
创建实时事件订阅 |
GET |
/event_subscriptions?version=1.0 |
列出事件订阅 |
事件类型
OnGuard 会生成以下类别的事件,并将其转发到 Google SecOps:
- “访问权限已授予”事件:在读卡器上接受了徽章刷卡
- “拒绝访问”事件:刷卡遭拒(卡无效、已过期、访问权限级别错误)
- 门保持打开状态事件:门保持打开状态的时间超过了配置的时间
- 门被强行打开事件:门在未刷有效门禁卡的情况下被打开
- 警报事件:警报输入已触发或已确认
- 状态事件:面板在线/离线、通信故障、篡改
- 持卡人事件:添加、修改或删除持卡人
- 徽章事件:徽章已激活、已停用或已丢失
- 访问者事件:访问者登录或退出
身份验证方法参考
Chronicle webhook Feed 支持多种身份验证方法。选择供应商支持的方法。
方法 1:自定义标头(推荐)
如果您的供应商支持自定义 HTTP 标头,请使用此方法以提高安全性。
请求格式:
POST <ENDPOINT_URL> HTTP/1.1 Content-Type: application/json x-goog-chronicle-auth: <API_KEY> x-chronicle-auth: <SECRET_KEY> { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
优点: - API 密钥和密钥不会显示在网址中 - 更安全(标头不会记录在 Web 服务器访问日志中) - 如果供应商支持,则首选此方法
方法 2:查询参数
如果您的供应商不支持自定义标头,请将凭据附加到网址。
网址格式:
<ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY>示例:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...&secret=abcd1234...请求格式:
POST <ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY> HTTP/1.1 Content-Type: application/json { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
缺点:
- 网址中显示的凭据
- 可能会记录在 Web 服务器访问日志中
- 安全性不如标头
方法 3:混合(网址 + 标头)
某些配置在网址中使用 API 密钥,在标头中使用密钥。
请求格式:
POST <ENDPOINT_URL>?key=<API_KEY> HTTP/1.1 Content-Type: application/json x-chronicle-auth: <SECRET_KEY> { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
身份验证标头名称
Chronicle 接受以下身份验证标头名称:
对于 API 密钥:
x-goog-chronicle-auth(推荐)X-Goog-Chronicle-Auth(不区分大小写)
对于密钥:
x-chronicle-auth(推荐)X-Chronicle-Auth(不区分大小写)
问题排查
OpenAccess API 连接失败
如果中间件无法连接到 OpenAccess API,请执行以下操作:
验证 OnGuard 服务器是否可从中间件主机访问:
curl -k https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess/version?version=1.0确认 OnGuard 服务器防火墙上的端口
8080已打开。在 OnGuard 服务器上,运行以下命令以验证端口是否正在侦听:
netstat -anb | findstr 8080如果端口未处于监听状态,请重启 LS OpenAccess 服务:
- 在 OnGuard 服务器上打开 Windows 服务 (
services.msc)。 - 右键点击 LS OpenAccess,然后选择重新启动。
- 在 OnGuard 服务器上打开 Windows 服务 (
身份验证失败
如果中间件收到身份验证错误:
- 验证 OnGuard 用户名和密码是否正确。
- 确认
ONGUARD_DIRECTORY_ID与GET /directories返回的有效目录匹配。 - 确保 OnGuard 用户账号未因暴力破解防护功能而被锁定。OnGuard 会在多次登录尝试失败后锁定账号。
- 如果
openaccess.ini文件位于C:\ProgramData\lnl\openaccess.ini,请验证其配置是否正确。
Chronicle 中未收到任何事件
如果中间件运行正常,但 Chronicle 中未显示任何事件,请执行以下操作:
- 通过检查 Alarm Monitoring 应用,验证 OnGuard 中是否正在生成事件。
确认已在 OnGuard 数据库中启用事件发布。在 OnGuard 数据库服务器上,运行以下 SQL 查询:
SELECT * FROM LNL_SYSTEMSETTINGS WHERE SETTINGNAME = 'EventPublishingDisabled'- 如果结果为
0或没有结果,则表示已启用事件发布。 - 如果结果为
1,则表示事件发布已停用,必须重新启用。
- 如果结果为
检查转发器日志文件 (
/opt/onguard-chronicle-forwarder/forwarder.log) 中是否有错误消息。验证
.env文件中的 Chronicle Webhook 端点网址、API 密钥和密钥是否正确。
请求池已满错误
如果中间件从 OpenAccess 收到 request pool full 错误:
- 在 OnGuard 服务器上,创建或修改文件
C:\ProgramData\Lnl\OpenAccess.ini。 添加以下内容:
[http_request] request_pool_size=128重启 LS OpenAccess 服务以使更改生效。
Webhook 限制和最佳实践
请求限制
| 限制 | 值 |
|---|---|
| 最大请求大小 | 4 MB |
| 最大 QPS(每秒查询次数) | 15000 |
| 请求超时 | 30 秒 |
| 重试行为 | 自动(使用指数退避算法) |
UDM 映射表
| 原始日志字段 | UDM 映射 | 逻辑 |
|---|---|---|
timestamp |
metadata.event_timestamp |
直接从活动时间戳映射 |
description |
metadata.description |
活动说明文本 |
event_type |
metadata.product_event_type |
OnGuard 事件类型标识符 |
badge_id |
principal.user.product_object_id |
与活动关联的徽章编号 |
cardholder_first_name |
principal.user.first_name |
持卡人的名字 |
cardholder_last_name |
principal.user.last_name |
持卡人的姓氏 |
device |
principal.asset.hostname |
读卡器或面板名称 |
source |
target.asset.hostname |
来源设备或面板 |
serial_number |
principal.asset.hardware.serial_number |
面板序列号 |
access_result |
security_result.action |
根据授予或拒绝的访问权限来允许或屏蔽 |
cardnumber", "device", "eid", "machine", "serialnum", and "site |
additional.fields |
从变更日志映射 |
更新日志
需要更多帮助?获得社区成员和 Google SecOps 专业人士的解答。