关于 issue-validator
用于 ServiceNow GRC 的议题验证技能,供合规团队在审计或状态变更前检查数据质量。它综合评估字段、风险、控制映射和整改计划,并生成准备度评分。
主要功能
- 01检查必填字段完整性
- 02核对风险评级与风险记录
- 03验证议题与控制的映射
- 04检查整改任务和计划
- 05生成审计准备度评分报告
适用场景
- 01审计委员会报告前批量检查议题
- 02定期开展 GRC 数据质量审查
- 03关闭议题前核实整改证据
MCP 相关
该技能通过 MCP 工具查询 ServiceNow GRC 议题、风险和控制,并运行后台脚本执行验证与评分。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill issue-validator安装后在 Claude Code 中输入/issue-validator确认已加载。
详细介绍
GRC 问题验证器
概述
此技能用于验证 ServiceNow 中 GRC 问题的完整性、准确性和合规就绪情况。它会从多个维度进行系统检查,确保问题在接受审计审查前符合组织和监管标准。
主要验证功能:
- 字段完整性: 验证所有必填字段均已填写(简短描述、类别、优先级、风险评级、配置文件、负责人)
- 风险评级准确性: 将风险评级与关联的风险评分和控制测试结果进行交叉核对
- 控制映射完整性: 确保问题已正确映射到相关控制和控制目标
- 整改计划充分性: 验证整改任务是否存在,以及是否具备负责人、截止日期和可衡量的结果
- 数据一致性: 检测问题严重程度与关联风险/控制状态之间的不匹配
- 审计就绪情况: 根据就绪度量表为问题评分,以供内部或外部审计展示
使用场景:
- 向审计委员会报告前,确保数据质量
- 定期进行 GRC 质量检查时
- 从自动扫描或评估中导入新问题时
- 关闭问题前,确认已收集所有整改证据
- 作为问题管理工作流中的质量关卡
前提条件
- 角色:
sn_grc.manager、sn_compliance.manager或admin - 插件:
com.sn_grc、com.sn_compliance、com.sn_risk - 访问权限: 对
sn_grc_issue、sn_grc_risk、sn_compliance_control、sn_grc_profile、sn_grc_remediation_task表具有读取权限 - 知识: 了解组织的问题管理政策和必填字段
主要 GRC 表
| 表 | 用途 | 关键字段 |
|---|---|---|
sn_grc_issue | GRC 问题和发现项 | number, short_description, state, priority, risk_rating, profile, item, category, source, assigned_to, due_date |
sn_grc_risk | 关联的风险记录 | number, risk_score, state, treatment, residual_risk, inherent_risk |
sn_compliance_control | 映射的控制 | number, state, control_objective, effectiveness, test_result |
sn_grc_profile | 实体配置文件 | number, profile_type, applies_to |
sn_grc_remediation_task | 问题的整改任务 | number, state, assigned_to, due_date, short_description, issue |
操作步骤
步骤 1:检索待验证的问题
获取目标问题,可以是单个问题,也可以是用于批量验证的一组问题。
使用 MCP(Claude Code/Desktop):
Tool: SN-Query-Table
Parameters:
table_name: sn_grc_issue
query: active=true^ORDERBYDESCsys_updated_on
fields: sys_id,number,short_description,description,state,priority,risk_rating,profile,item,assigned_to,due_date,category,source,remediation_plan,sys_created_on,sys_updated_on,closed_at,close_notes
limit: 50
使用 REST API:
GET /api/now/table/sn_grc_issue?sysparm_query=active=true^ORDERBYDESCsys_updated_on&sysparm_fields=sys_id,number,short_description,description,state,priority,risk_rating,profile,item,assigned_to,due_date,category,source,remediation_plan&sysparm_limit=50&sysparm_display_value=all
步骤 2:验证字段完整性
对所有目标问题进行全面的字段完整性检查。
使用 MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var requiredFields = [
'short_description', 'description', 'priority', 'risk_rating',
'profile', 'assigned_to', 'due_date', 'category', 'source'
];
var results = { total_checked: 0, passed: 0, failed: 0, issues: [] };
var gr = new GlideRecord('sn_grc_issue');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
results.total_checked++;
var missing = [];
for (var i = 0; i < requiredFields.length; i++) {
var field = requiredFields[i];
if (gr.getValue(field) === null || gr.getValue(field) === '') {
missing.push(field);
}
}
if (missing.length > 0) {
results.failed++;
results.issues.push({
number: gr.number.toString(),
title: gr.short_description.toString(),
missing_fields: missing,
completeness_pct: Math.round(((requiredFields.length - missing.length) / requiredFields.length) * 100)
});
} else {
results.passed++;
}
}
results.pass_rate = results.total_checked > 0
? Math.round((results.passed / results.total_checked) * 100) + '%'
: 'N/A';
gs.info('FIELD COMPLETENESS VALIDATION:\n' + JSON.stringify(results, null, 2));
description: "GRC: Validate field completeness for all active issues"
使用 REST API:
GET /api/now/table/sn_grc_issue?sysparm_query=active=true^short_descriptionISEMPTY^ORassigned_toISEMPTY^ORdue_dateISEMPTY^ORcategoryISEMPTY^ORprofileISEMPTY&sysparm_fields=sys_id,number,short_description,assigned_to,due_date,category,profile&sysparm_limit=100
步骤 3:验证风险评级一致性
将问题的风险评级与关联风险记录的评分进行交叉核对,以检测不匹配。
使用 MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var mismatches = [];
var gr = new GlideRecord('sn_grc_issue');
gr.addQuery('active', true);
gr.addNotNullQuery('risk_rating');
gr.addNotNullQuery('profile');
gr.query();
while (gr.next()) {
var issueRating = parseInt(gr.risk_rating.toString()) || 0;
var profileId = gr.profile.toString();
var risk = new GlideAggregate('sn_grc_risk');
risk.addQuery('profile', profileId);
risk.addQuery('active', true);
risk.addAggregate('MAX', 'residual_risk');
risk.addAggregate('AVG', 'risk_score');
risk.query();
if (risk.next()) {
var maxResidual = parseInt(risk.getAggregate('MAX', 'residual_risk')) || 0;
var avgScore = parseInt(risk.getAggregate('AVG', 'risk_score')) || 0;
// Flag if issue rating is significantly lower than risk data suggests
if (issueRating <= 2 && maxResidual >= 70) {
mismatches.push({
issue: gr.number.toString(),
issue_risk_rating: issueRating,
max_residual_risk: maxResidual,
avg_risk_score: avgScore,
flag: 'Issue rated LOW but linked risks are HIGH'
});
} else if (issueRating >= 4 && maxResidual <= 30) {
mismatches.push({
issue: gr.number.toString(),
issue_risk_rating: issueRating,
max_residual_risk: maxResidual,
avg_risk_score: avgScore,
flag: 'Issue rated HIGH but linked risks are LOW'
});
}
}
}
gs.info('RISK RATING CONSISTENCY:\nMismatches found: ' + mismatches.length + '\n' + JSON.stringify(mismatches, null, 2));
description: "GRC: Cross-validate risk ratings against linked risk records"
步骤 4:验证控制映射
确保每个问题至少映射到一个控制,且映射的控制处于有效状态。
使用 MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var controlIssues = { unmapped: [], stale_controls: [], orphaned: [] };
var gr = new GlideRecord('sn_grc_issue');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
var profileId = gr.profile.toString();
if (!profileId) {
controlIssues.unmapped.push({
issue: gr.number.toString(),
problem: 'No profile linked - cannot verify control mapping'
});
continue;
}
var ctrl = new GlideAggregate('sn_compliance_control');
ctrl.addQuery('profile', profileId);
ctrl.addAggregate('COUNT');
ctrl.query();
var controlCount = 0;
if (ctrl.next()) {
controlCount = parseInt(ctrl.getAggregate('COUNT')) || 0;
}
if (controlCount === 0) {
controlIssues.unmapped.push({
issue: gr.number.toString(),
profile: gr.profile.getDisplayValue(),
problem: 'Profile has no linked controls'
});
}
// Check for stale control test results (over 12 months old)
var stale = new GlideRecord('sn_compliance_control');
stale.addQuery('profile', profileId);
stale.addQuery('test_date', '<', gs.daysAgo(365));
stale.query();
while (stale.next()) {
controlIssues.stale_controls.push({
issue: gr.number.toString(),
control: stale.number.toString(),
last_tested: stale.test_date.getDisplayValue()
});
}
}
gs.info('CONTROL MAPPING VALIDATION:\n' + JSON.stringify(controlIssues, null, 2));
description: "GRC: Validate control mappings for active issues"
步骤 5:验证整改计划充分性
检查每个问题是否至少有一个整改任务,并且该任务已指定负责人和截止日期。
使用 MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var remediationGaps = [];
var gr = new GlideRecord('sn_grc_issue');
gr.addQuery('active', true);
gr.addQuery('state', 'NOT IN', 'new,draft');
gr.query();
while (gr.next()) {
var tasks = new GlideRecord('sn_grc_remediation_task');
tasks.addQuery('issue', gr.sys_id.toString());
tasks.query();
var taskCount = 0;
var tasksWithoutOwner = 0;
var tasksWithoutDueDate = 0;
var overdueTasks = 0;
var now = new GlideDateTime();
while (tasks.next()) {
taskCount++;
if (!tasks.assigned_to.toString()) tasksWithoutOwner++;
if (!tasks.due_date.toString()) tasksWithoutDueDate++;
if (tasks.due_date.toString() && new GlideDateTime(tasks.due_date.toString()).compareTo(now) < 0 && tasks.state.toString() != 'closed') {
overdueTasks++;
}
}
var gaps = [];
if (taskCount === 0) gaps.push('No remediation tasks defined');
if (tasksWithoutOwner > 0) gaps.push(tasksWithoutOwner + ' task(s) without owner');
if (tasksWithoutDueDate > 0) gaps.push(tasksWithoutDueDate + ' task(s) without due date');
if (overdueTasks > 0) gaps.push(overdueTasks + ' overdue task(s)');
if (gaps.length > 0) {
remediationGaps.push({
issue: gr.number.toString(),
priority: gr.priority.getDisplayValue(),
task_count: taskCount,
gaps: gaps
});
}
}
gs.info('REMEDIATION PLAN VALIDATION:\nIssues with gaps: ' + remediationGaps.length + '\n' + JSON.stringify(remediationGaps, null, 2));
description: "GRC: Validate remediation plans for active issues"
步骤 6:生成综合验证报告
将所有验证结果汇总为一份统一的评分报告。
使用 MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var report = {
generated: new GlideDateTime().toString(),
summary: { total_issues: 0, fully_valid: 0, needs_attention: 0 },
scores: [],
recommendations: []
};
var gr = new GlideRecord('sn_grc_issue');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
report.summary.total_issues++;
var score = 100;
var deductions = [];
// Field completeness (-10 per missing required field)
var reqFields = ['short_description','description','priority','risk_rating','profile','assigned_to','due_date','category'];
for (var i = 0; i < reqFields.length; i++) {
if (!gr.getValue(reqFields[i])) {
score -= 10;
deductions.push('Missing: ' + reqFields[i]);
}
}
// Due date validation (-15 if overdue)
if (gr.due_date.toString() && new GlideDateTime(gr.due_date.toString()).compareTo(new GlideDateTime()) < 0) {
score -= 15;
deductions.push('Issue is overdue');
}
// Remediation check (-20 if no tasks)
var taskGr = new GlideAggregate('sn_grc_remediation_task');
taskGr.addQuery('issue', gr.sys_id.toString());
taskGr.addAggregate('COUNT');
taskGr.query();
var taskCount = 0;
if (taskGr.next()) taskCount = parseInt(taskGr.getAggregate('COUNT'));
if (taskCount === 0) {
score -= 20;
deductions.push('No remediation tasks');
}
if (score < 0) score = 0;
if (score >= 80) report.summary.fully_valid++;
else report.summary.needs_attention++;
if (score < 80) {
report.scores.push({
issue: gr.number.toString(),
title: gr.short_description.toString(),
validation_score: score,
deductions: deductions
});
}
}
report.scores.sort(function(a, b) { return a.validation_score - b.validation_score; });
gs.info('GRC ISSUE VALIDATION REPORT:\n' + JSON.stringify(report, null, 2));
description: "GRC: Generate comprehensive validation report with scores"
工具用法
| 操作 | MCP 工具 | REST 端点 |
|---|---|---|
| 查询问题 | SN-Query-Table | GET /api/now/table/sn_grc_issue |
| 读取单个问题 | SN-Get-Record | GET /api/now/table/sn_grc_issue/{sys_id} |
| 运行验证脚本 | SN-Execute-Background-Script | POST /api/now/table/sys_trigger |
| 探索架构 | SN-Discover-Table-Schema | GET /api/now/table/sys_dictionary |
| 查询控制 | SN-Query-Table | GET /api/now/table/sn_compliance_control |
最佳实践
- 定期运行验证: 安排每周或每两周进行一次全面验证,而不只是审计前验证
- 集中定义验证规则: 在共享的合规标准中记录必填字段和阈值,确保验证人员使用一致的标准
- 评分并确定优先级: 使用验证评分确定数据质量问题修复的优先级——优先解决关键缺口
- 尽可能自动化: 将重复的验证检查转化为业务规则或定时作业,以便持续监控
- 跟踪趋势: 监控验证通过率随时间的变化,以衡量数据质量的改善情况
- 状态转换前进行验证: 在问题转为“In Remediation”或“Closed”状态前强制执行验证检查
- 在报告中提供背景信息: 始终为验证失败项附上可执行的修复指导
故障排除
未找到修复任务表
症状: 查询 sn_grc_remediation_task 无结果,或提示找不到表
原因: 不同的 ServiceNow 版本或自定义配置可能使用不同的修复任务表名
解决方案:
Tool: SN-Discover-Table-Schema
Parameters:
table_name: sn_grc_issue
检查指向任务表的引用字段。其他可能的表名包括 sn_grc_task、task 或自定义扩展任务表。
风险评级字段使用不同的量表
症状: 风险评级一致性检查产生误报
原因: 有些组织使用 1-5 量表,其他组织则使用 1-100 量表或 Low/Medium/High 标签
解决方案: 查询 sys_choice 表中的 sn_grc_issue.risk_rating,以确定有效值,并相应调整验证阈值。
基于档案的查询未返回关联记录
症状: 按档案 sys_id 查询时找不到控制措施或风险
原因: 关联关系可能使用 item 而非 profile,也可能通过多对多关系表建立
解决方案: 检查 sn_grc_m2m_item_profile,或改为按问题上的 item 字段查询,而非 profile。
示例
示例 1:审计前批量验证
场景: 合规经理需要在外部审计前验证所有未解决的问题
- 查询全部 85 条有效的 GRC 问题
- 运行字段完整性检查:72 条通过(85%),13 条未通过
- 运行风险评级一致性检查:标记出 4 处不一致
- 运行控制措施映射检查:6 个问题未关联控制措施
- 运行修复计划检查:11 个问题缺少修复任务
- 生成评分报告:62 个问题得分达到 80 分以上,23 个需要关注
- 首要发现:5 个最高优先级问题没有指定负责人
示例 2:关闭前验证单个问题
场景: 问题负责人想确认 ISSUE0004521 是否已满足关闭条件
- 读取包含所有字段的问题记录
- 字段完整性:100%——所有必填字段均已填写
- 风险评级:评级为“High”(4),关联风险的剩余分数为 72——一致
- 控制措施映射:关联了 3 项控制措施,均处于“Compliant”状态
- 修复情况:2 项任务均已关闭,并附有完成证据
- 验证评分:95/100(轻微扣分:关闭备注可以写得更详细)
- 结果:批准关闭问题
相关技能
grc/issue-summarization- 汇总已验证的问题以供报告grc/issue-action-plan- 为未通过验证的问题生成行动计划grc/suggest-remediation-tasks- 为缺少修复任务的问题创建修复任务grc/control-objective-management- 管理与问题关联的控制措施grc/risk-assessment-summarization- 深入分析风险,以验证评级