如何排查无源代码的自定义 CLR 审计触发器?
结论
SQL Server 2019 可以运行 CLR trigger,CLR enabled 配置也不是本例的根因。该 CLR 组件会查询 INFORMATION_SCHEMA.TABLES。新数据库安装 tSQLt 后多出了一些对象,而组件没有妥善处理相应的对象匹配或结果集变化,导致部分表的审计逻辑失败。
确认该数据库不再需要 tSQLt 测试对象后,可执行以下命令卸载 tSQLt。卸载后,CLR trigger 即可恢复:
EXEC tSQLt.Uninstall;
CLR enabled 只能说明 SQL Server 允许执行 CLR 代码,无法证明触发器中的查询、过滤条件和异常处理仍适用于新数据库。
取得完整异常信息
仅检查业务程序是否向 audit table 写入记录并不足以判断问题,因为应用程序可能捕获并隐藏数据库异常。可以在 SSMS 中直接对异常表执行最小化的 INSERT、UPDATE 或 DELETE,查看 SQL Server 返回的完整错误信息:
BEGIN TRANSACTION;
INSERT INTO dbo.TargetTable (Column1, Column2)
VALUES ('Test', 1);
ROLLBACK TRANSACTION;
如果触发器执行失败,请保存以下信息:
- 完整的错误号和错误文本;
- CLR 异常类型及调用栈;
- 发生异常的程序集、类和方法;
- 触发器执行过程中使用的 SQL;
- 操作是否随事务一起回滚。
题目中只保留了 “Error”,没有实际错误文本,暂时无法据此判断具体的异常类型。
检查触发器与程序集映射
先比较正常表和异常表的触发器配置,确认它们是否绑定到同一个程序集、类和方法:
SELECT
s.name AS schema_name,
o.name AS table_name,
tr.name AS trigger_name,
tr.is_disabled,
tr.is_instead_of_trigger,
tr.is_not_for_replication,
a.name AS assembly_name,
a.permission_set_desc,
am.assembly_class,
am.assembly_method
FROM sys.triggers AS tr
JOIN sys.objects AS o
ON tr.parent_id = o.object_id
JOIN sys.schemas AS s
ON o.schema_id = s.schema_id
LEFT JOIN sys.assembly_modules AS am
ON tr.object_id = am.object_id
LEFT JOIN sys.assemblies AS a
ON am.assembly_id = a.assembly_id
WHERE tr.parent_class = 1
ORDER BY s.name, o.name, tr.name;
然后检查各触发器实际订阅的事件:
SELECT
tr.name AS trigger_name,
te.type_desc AS event_type
FROM sys.triggers AS tr
JOIN sys.trigger_events AS te
ON tr.object_id = te.object_id
WHERE tr.parent_class = 1
ORDER BY tr.name, te.type_desc;
通过这些查询,可以检查触发器是否被禁用、事件是否绑定,以及程序集版本或调用入口是否存在差异。
比较两个数据库的对象
同一个 CLR trigger 只在部分表上失败时,应检查代码是否依赖元数据查询、对象名称、schema 或列结构。先查看新数据库中 INFORMATION_SCHEMA.TABLES 的实际查询结果:
SELECT
TABLE_CATALOG,
TABLE_SCHEMA,
TABLE_NAME,
TABLE_TYPE
FROM INFORMATION_SCHEMA.TABLES
ORDER BY TABLE_SCHEMA, TABLE_NAME;
再单独检查 tSQLt 创建的对象:
SELECT
s.name AS schema_name,
o.name AS object_name,
o.type_desc
FROM sys.objects AS o
JOIN sys.schemas AS s
ON o.schema_id = s.schema_id
WHERE s.name = N'tSQLt'
OR o.name LIKE N'tSQLt%'
ORDER BY s.name, o.name;
本例的 CLR 代码查询了 INFORMATION_SCHEMA.TABLES。安装 tSQLt 后,查询结果中出现了代码未预料到的对象。冲突的具体方式取决于程序集实现,可能是只按对象名匹配而未限定 schema,也可能是代码假定查询只返回一行,或遍历结果时没有过滤测试对象。
没有源代码时检查程序集
如果组织拥有该程序集并有权分析,可以使用 dotPeek、ILSpy 等反编译工具查看 DLL。检查时可优先搜索:
INFORMATION_SCHEMA、sys.tables和sys.objects;- audit table 名称;
- 按表名或 schema 过滤的代码;
SqlContext、SqlCommand和动态 SQL;- 捕获后没有重新抛出的异常;
- 对列顺序、字段数量或单行结果的固定假设。
反编译结果未必与原始源码完全一致,但通常足以找到 SQL 字符串和相关条件分支。操作前应确认程序集归属和授权范围。
处理 tSQLt 冲突
如果 tSQLt 只是临时安装,且数据库已不再需要这些测试对象,可以先备份数据库,然后在目标数据库中执行:
EXEC tSQLt.Uninstall;
这条命令会从当前数据库中卸载 tSQLt,不是暂时关闭测试功能。如果仍需保留测试,可以修改 CLR 代码,例如:
SELECT
TABLE_SCHEMA,
TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
AND TABLE_SCHEMA <> 'tSQLt';
过滤条件应根据业务 schema 和审计范围确定。如果代码按名称查找表,还需要限定 TABLE_SCHEMA,以免不同 schema 下的同名对象产生歧义。
注意事项
不能直接把 SQL Server 2016 与 2019 的版本差异认定为根因。排查时应比较程序集、触发器定义、数据库对象、schema、权限和测试框架的安装情况。
审计触发器还应支持一条语句影响多行的情况,不能假定 inserted 或 deleted 中只有一行。修复后需要分别测试单行和批量的 INSERT、UPDATE、DELETE,并确认业务数据与 audit table 的事务结果符合预期。
备注:内容仅供参考。
