skills/grc/risk-assessment-summarization/SKILL.md
Summarize risk assessments with scoring, trends, heat map data, and mitigation recommendations covering inherent vs residual risk analysis
npx skillsauth add happy-technologies-llc/happy-servicenow-skills risk-assessment-summarizationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill produces comprehensive summaries of risk assessments in ServiceNow GRC, including:
When to use:
sn_risk.manager, sn_grc.manager, or admincom.sn_risk, com.sn_grc| Table | Purpose | Key Fields |
|-------|---------|------------|
| sn_grc_risk | Risk register records | number, short_description, risk_score, inherent_risk, residual_risk, state, treatment, likelihood, impact, profile, category, owner |
| sn_risk_definition | Risk framework definitions | number, short_description, risk_criteria, scoring_method, appetite |
| sn_risk_event | Actual risk events/incidents | number, short_description, event_date, impact_amount, risk, category |
| sn_grc_profile | Entity profiles for risk scoping | number, short_description, profile_type, applies_to |
| sn_grc_item | GRC content items | number, short_description, item_type, profile |
Query all active risks with scoring data.
Using MCP (Claude Code/Desktop):
Tool: SN-Query-Table
Parameters:
table_name: sn_grc_risk
query: active=true^ORDERBYDESCrisk_score
fields: sys_id,number,short_description,risk_score,inherent_risk,residual_risk,state,treatment,likelihood,impact,profile,category,owner,sys_created_on,sys_updated_on
limit: 100
Using REST API:
GET /api/now/table/sn_grc_risk?sysparm_query=active=true^ORDERBYDESCrisk_score&sysparm_fields=sys_id,number,short_description,risk_score,inherent_risk,residual_risk,state,treatment,likelihood,impact,profile,category,owner&sysparm_limit=100
Understand the scoring methodology and risk appetite thresholds.
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: sn_risk_definition
query: active=true
fields: sys_id,number,short_description,risk_criteria,scoring_method,appetite,description
limit: 20
Using REST API:
GET /api/now/table/sn_risk_definition?sysparm_query=active=true&sysparm_fields=sys_id,number,short_description,risk_criteria,scoring_method,appetite,description&sysparm_limit=20
Produce aggregated risk metrics and control effectiveness:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var summary = {
generated_date: new GlideDateTime().toString(),
total_risks: 0,
by_treatment: {},
by_category: {},
inherent_vs_residual: {
avg_inherent: 0,
avg_residual: 0,
control_effectiveness_pct: 0,
risks_above_appetite: 0
},
severity_distribution: { critical: 0, high: 0, medium: 0, low: 0 },
top_risks: []
};
var inherentSum = 0, residualSum = 0, count = 0;
var riskAppetite = 50; // Default; adjust per organization
var gr = new GlideRecord('sn_grc_risk');
gr.addQuery('active', true);
gr.orderByDesc('risk_score');
gr.query();
while (gr.next()) {
summary.total_risks++;
count++;
var inherent = parseInt(gr.inherent_risk.toString()) || 0;
var residual = parseInt(gr.residual_risk.toString()) || 0;
var score = parseInt(gr.risk_score.toString()) || 0;
inherentSum += inherent;
residualSum += residual;
if (residual > riskAppetite) summary.inherent_vs_residual.risks_above_appetite++;
// Severity distribution
if (score >= 80) summary.severity_distribution.critical++;
else if (score >= 60) summary.severity_distribution.high++;
else if (score >= 30) summary.severity_distribution.medium++;
else summary.severity_distribution.low++;
// Treatment breakdown
var treat = gr.treatment.getDisplayValue() || 'Not Assigned';
summary.by_treatment[treat] = (summary.by_treatment[treat] || 0) + 1;
// Category breakdown
var cat = gr.category.getDisplayValue() || 'Uncategorized';
summary.by_category[cat] = (summary.by_category[cat] || 0) + 1;
// Top 10 risks
if (summary.top_risks.length < 10) {
summary.top_risks.push({
number: gr.number.toString(),
title: gr.short_description.toString(),
risk_score: score,
inherent: inherent,
residual: residual,
treatment: gr.treatment.getDisplayValue(),
owner: gr.owner.getDisplayValue()
});
}
}
if (count > 0) {
summary.inherent_vs_residual.avg_inherent = Math.round(inherentSum / count);
summary.inherent_vs_residual.avg_residual = Math.round(residualSum / count);
if (inherentSum > 0) {
summary.inherent_vs_residual.control_effectiveness_pct =
Math.round(((inherentSum - residualSum) / inherentSum) * 100);
}
}
gs.info('RISK ASSESSMENT SUMMARY:\n' + JSON.stringify(summary, null, 2));
description: "GRC: Generate risk assessment summary with inherent vs residual analysis"
Build a likelihood-impact matrix for heat map visualization:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Heat map: 5x5 matrix (likelihood x impact)
var heatMap = {};
for (var l = 1; l <= 5; l++) {
for (var i = 1; i <= 5; i++) {
heatMap[l + 'x' + i] = { likelihood: l, impact: i, count: 0, risks: [] };
}
}
var gr = new GlideRecord('sn_grc_risk');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
var likelihood = parseInt(gr.likelihood.toString()) || 0;
var impact = parseInt(gr.impact.toString()) || 0;
// Normalize to 1-5 scale if needed
if (likelihood > 5) likelihood = Math.ceil(likelihood / 20);
if (impact > 5) impact = Math.ceil(impact / 20);
if (likelihood < 1) likelihood = 1;
if (impact < 1) impact = 1;
var key = likelihood + 'x' + impact;
if (heatMap[key]) {
heatMap[key].count++;
if (heatMap[key].risks.length < 3) {
heatMap[key].risks.push(gr.number.toString() + ': ' + gr.short_description.toString());
}
}
}
// Format for output
var matrix = [];
for (var cell in heatMap) {
if (heatMap[cell].count > 0) {
matrix.push(heatMap[cell]);
}
}
matrix.sort(function(a, b) { return (b.likelihood * b.impact) - (a.likelihood * a.impact); });
gs.info('RISK HEAT MAP DATA:\n' + JSON.stringify(matrix, null, 2));
gs.info('\nHEAT MAP LEGEND:');
gs.info(' Critical (Red): Likelihood >= 4 AND Impact >= 4');
gs.info(' High (Orange): Score >= 12');
gs.info(' Medium (Yellow): Score >= 6');
gs.info(' Low (Green): Score < 6');
description: "GRC: Generate risk heat map data (likelihood x impact matrix)"
Compare current risk posture to historical baseline:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var periods = [
{ label: 'Current', days: 0 },
{ label: '30 Days Ago', days: 30 },
{ label: '60 Days Ago', days: 60 },
{ label: '90 Days Ago', days: 90 }
];
var trends = [];
for (var p = 0; p < periods.length; p++) {
var period = periods[p];
var totalScore = 0, count = 0, critical = 0;
if (period.days === 0) {
var gr = new GlideRecord('sn_grc_risk');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
var s = parseInt(gr.risk_score.toString()) || 0;
totalScore += s;
count++;
if (s >= 80) critical++;
}
} else {
// Use risk events as proxy for historical risk levels
var ev = new GlideAggregate('sn_risk_event');
ev.addQuery('sys_created_on', '>=', gs.daysAgo(period.days));
ev.addQuery('sys_created_on', '<', gs.daysAgo(period.days - 30));
ev.addAggregate('COUNT');
ev.query();
if (ev.next()) count = parseInt(ev.getAggregate('COUNT'));
}
trends.push({
period: period.label,
total_risks: count,
avg_score: count > 0 ? Math.round(totalScore / count) : 0,
critical_risks: critical
});
}
gs.info('RISK TREND ANALYSIS:\n' + JSON.stringify(trends, null, 2));
description: "GRC: Analyze risk score trends over time"
Produce actionable recommendations based on risk treatment and residual exposure:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var recommendations = [];
var gr = new GlideRecord('sn_grc_risk');
gr.addQuery('active', true);
gr.addQuery('residual_risk', '>', 50);
gr.orderByDesc('residual_risk');
gr.setLimit(20);
gr.query();
while (gr.next()) {
var inherent = parseInt(gr.inherent_risk.toString()) || 0;
var residual = parseInt(gr.residual_risk.toString()) || 0;
var reduction = inherent > 0 ? Math.round(((inherent - residual) / inherent) * 100) : 0;
var treatment = gr.treatment.getDisplayValue() || 'Not Assigned';
var rec = {
risk_number: gr.number.toString(),
title: gr.short_description.toString(),
inherent_risk: inherent,
residual_risk: residual,
risk_reduction_pct: reduction,
current_treatment: treatment,
recommendations: []
};
// Generate recommendations based on treatment and residual level
if (treatment === 'Accept' && residual >= 70) {
rec.recommendations.push('Re-evaluate acceptance decision; residual risk exceeds organizational appetite');
rec.recommendations.push('Conduct cost-benefit analysis for additional controls');
}
if (treatment === 'Mitigate' && reduction < 30) {
rec.recommendations.push('Current controls are insufficient; risk reduction is only ' + reduction + '%');
rec.recommendations.push('Evaluate additional compensating controls or alternative mitigation strategies');
}
if (treatment === 'Transfer' && residual >= 60) {
rec.recommendations.push('Verify insurance coverage adequacy for residual exposure');
rec.recommendations.push('Review third-party SLAs and liability clauses');
}
if (treatment === 'Not Assigned') {
rec.recommendations.push('URGENT: Assign risk treatment strategy immediately');
rec.recommendations.push('Schedule risk owner meeting to determine treatment approach');
}
if (residual >= 80) {
rec.recommendations.push('CRITICAL: Escalate to executive leadership for immediate action');
}
// Check for related control failures
var ctrl = new GlideRecord('sn_compliance_control');
ctrl.addQuery('profile', gr.profile.toString());
ctrl.addQuery('state', 'NOT IN', 'compliant,passed');
ctrl.query();
if (ctrl.getRowCount() > 0) {
rec.recommendations.push('Address ' + ctrl.getRowCount() + ' failing controls linked to this risk profile');
}
recommendations.push(rec);
}
gs.info('MITIGATION RECOMMENDATIONS:\n' + JSON.stringify(recommendations, null, 2));
description: "GRC: Generate mitigation recommendations for high-residual risks"
Retrieve actual risk events to ground the assessment in real incidents:
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: sn_risk_event
query: sys_created_on>=javascript:gs.daysAgo(365)^ORDERBYDESCimpact_amount
fields: sys_id,number,short_description,event_date,impact_amount,risk,category,state
limit: 50
Using REST API:
GET /api/now/table/sn_risk_event?sysparm_query=sys_created_on>=javascript:gs.daysAgo(365)^ORDERBYDESCimpact_amount&sysparm_fields=sys_id,number,short_description,event_date,impact_amount,risk,category,state&sysparm_limit=50
| Operation | MCP Tool | REST Endpoint | |-----------|----------|---------------| | Query Risks | SN-Query-Table | GET /api/now/table/sn_grc_risk | | Query Risk Definitions | SN-Query-Table | GET /api/now/table/sn_risk_definition | | Query Risk Events | SN-Query-Table | GET /api/now/table/sn_risk_event | | Search Risks | SN-Natural-Language-Search | N/A | | Complex Analysis | SN-Execute-Background-Script | POST /api/now/table/sys_trigger | | Schema Discovery | SN-Discover-Table-Schema | GET /api/now/table/sys_dictionary |
Symptom: All risk_score, inherent_risk, or residual_risk fields return 0 or empty Cause: Risk scoring may use calculated fields populated by assessment workflows, or custom scoring fields Solution:
Tool: SN-Discover-Table-Schema
Parameters:
table_name: sn_grc_risk
Check for alternative scoring fields like calculated_risk, risk_score_calculated, or assessment_score. Also verify that risk assessments have been completed.
Symptom: Heat map generation fails due to empty likelihood/impact values
Cause: Risk records may use qualitative ratings (High/Medium/Low) instead of numeric scores
Solution: Map qualitative values to numeric scale: Critical=5, High=4, Medium=3, Low=2, Very Low=1. Check the sys_choice table for field value definitions.
Symptom: No records returned from sn_risk_event
Cause: The Risk Events module may not be configured, or events are tracked in a custom table
Solution: Check if com.sn_risk_advanced plugin is activated. Some organizations track loss events in custom tables or external systems.
Scenario: CRO needs a consolidated risk posture report for the board
Tool: SN-Natural-Language-Search
Parameters:
table_name: sn_grc_risk
query: "all active risks with high or critical risk scores grouped by category"
limit: 50
Generated Summary:
Scenario: Risk committee requests a visual risk landscape
Heat Map Output (top-right = highest concern):
Impact → 1 2 3 4 5
Likelihood
5 [ 0] [ 2] [ 5] [ 8] [ 3] ← Highest concern row
4 [ 1] [ 4] [12] [15] [ 6]
3 [ 3] [ 8] [18] [10] [ 4]
2 [ 5] [12] [ 9] [ 6] [ 2]
1 [ 8] [ 7] [ 5] [ 2] [ 1]
grc/issue-action-plan - Create action plans for risks that have materialized as issuesgrc/issue-summarization - Summarize issues arising from risk assessmentsgrc/control-objective-management - Manage controls that mitigate assessed risksgrc/regulatory-alert-analysis - Assess regulatory changes impacting risk landscapesecurity/audit-compliance - Audit controls supporting risk mitigationtesting
Manage supplier onboarding, qualification, performance monitoring, and offboarding with auditable lifecycle controls
tools
Identify emerging risks, prioritize intake signals, and route candidates into formal GRC risk assessment workflows
documentation
Screen inbound documents for completeness, policy risk, and routing readiness before extraction or case workflows
testing
Generate concise task summaries with status, timeline, blockers, SLA risk, and recommended next actions