.github/skills/heatmap-visualization/SKILL.md
Use this skill when asked to create heatmaps, visualize patterns over time, show activity grids, or display aggregated data in a matrix format. Triggers on keywords like "heatmap", "show heatmap", "visualize patterns", "activity grid", "time-based visualization", or when analyzing attack patterns, sign-in activity, or event distributions by time period.
npx skillsauth add scstelz/security-investigator heatmap-visualizationInstall 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.
Generate interactive heatmap visualizations from Microsoft Sentinel data using the Sentinel Heatmap MCP App. Heatmaps display aggregated data in a row/column grid with color-coded intensity, ideal for identifying patterns across time periods, comparing entities, or spotting anomalies.
# 1. Query Sentinel for aggregated data
mcp_sentinel-data_query_lake({
"query": "SigninLogs | where TimeGenerated > ago(24h) | summarize value = count() by row = AppDisplayName, column = format_datetime(bin(TimeGenerated, 1h), 'HH:mm') | project row, column, value"
})
# 2. Display heatmap
mcp_sentinel-heat_show-signin-heatmap({
"data": [<query results>],
"title": "Sign-Ins by Application (Last 24h)",
"rowLabel": "Application",
"colLabel": "Hour (UTC)",
"valueLabel": "Sign-ins",
"colorScale": "green-red"
})
mcp_sentinel-heat_show-signin-heatmap| Parameter | Required | Type | Description |
|-----------|----------|------|-------------|
| data | ✅ | array | Array of {row, column, value} objects |
| title | ❌ | string | Title displayed above heatmap |
| rowLabel | ❌ | string | Label for row axis (e.g., "IP Address") |
| colLabel | ❌ | string | Label for column axis (e.g., "Hour") |
| valueLabel | ❌ | string | Label for cell values (e.g., "Events") |
| colorScale | ❌ | string | green-red, blue-red, or blue-yellow |
| enrichment | ❌ | array | IP enrichment data for click-to-expand panels |
{
"data": [
{"row": "192.168.1.1", "column": "10:00", "value": 45},
{"row": "192.168.1.1", "column": "11:00", "value": 62},
{"row": "10.0.0.5", "column": "10:00", "value": 128}
]
}
{
"enrichment": [
{
"ip": "80.94.95.83",
"city": "Timișoara",
"country": "RO",
"org": "AS204428 SS-Net",
"is_vpn": false,
"abuse_confidence_score": 100,
"total_reports": 975,
"last_reported": "2026-01-29",
"threat_categories": ["RDP Brute-Force", "Hacking", "Port Scan"]
}
]
}
All queries must return row, column, value columns.
<Table>
| where TimeGenerated between (datetime(<start>) .. datetime(<end>))
| summarize value = count()
by row = <entity_field>,
column = format_datetime(bin(TimeGenerated, 1h), "HH:mm")
| project row, column, value
| order by column asc
<Table>
| where TimeGenerated > ago(30d)
| summarize value = count()
by row = <entity_field>,
column = format_datetime(bin(TimeGenerated, 1d), "yyyy-MM-dd")
| project row, column, value
| order by column asc
<Table>
| where TimeGenerated > ago(7d)
| summarize value = count()
by row = <dimension1>,
column = <dimension2>
| project row, column, value
| order by value desc
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0 // Successful sign-ins
| summarize value = count()
by row = AppDisplayName,
column = format_datetime(bin(TimeGenerated, 1h), "HH:mm")
| project row, column, value
| order by column asc
Recommended: colorScale: "green-red" (activity = good)
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != 0 // Failed sign-ins
| summarize value = count()
by row = IPAddress,
column = format_datetime(bin(TimeGenerated, 1h), "HH:mm")
| project row, column, value
| order by column asc, value desc
| take 500 // Limit to top patterns
Recommended: colorScale: "blue-red" (failures = threat)
let start = datetime(<StartDate>);
let end = datetime(<EndDate>);
let honeypot = '<HONEYPOT_NAME>';
SecurityEvent
| where TimeGenerated between (start .. end)
| where Computer contains honeypot
| where EventID in (4625, 4771, 4776) // Failed auth events
| where isnotempty(IpAddress) and IpAddress != "-" and IpAddress != "127.0.0.1"
| summarize value = count()
by row = IpAddress,
column = format_datetime(bin(TimeGenerated, 1h), "HH:mm")
| project row, column, value
| order by column asc, value desc
Recommended: colorScale: "blue-red" (attacks = threat)
let start = datetime(<StartDate>);
let end = datetime(<EndDate>);
W3CIISLog
| where TimeGenerated between (start .. end)
| where tolong(scStatus) >= 400 // HTTP errors
| where cIP != "127.0.0.1"
| summarize value = count()
by row = cIP,
column = format_datetime(bin(TimeGenerated, 1h), "HH:mm")
| project row, column, value
| order by column asc, value desc
| take 300
Recommended: colorScale: "blue-red"
SecurityAlert
| where TimeGenerated > ago(30d)
| summarize value = count()
by row = AlertSeverity,
column = format_datetime(bin(TimeGenerated, 1d), "yyyy-MM-dd")
| project row, column, value
| order by column asc
Recommended: colorScale: "blue-yellow" (neutral overview)
SigninLogs
| where TimeGenerated > ago(7d)
| where UserPrincipalName =~ '<UPN>'
| summarize value = count()
by row = AppDisplayName,
column = format_datetime(bin(TimeGenerated, 1d), "MM-dd")
| project row, column, value
| order by column asc
Recommended: colorScale: "green-red"
let start = datetime(<StartDate>);
let end = datetime(<EndDate>);
let honeypot = '<HONEYPOT_NAME>';
union
(SecurityEvent
| where TimeGenerated between (start .. end)
| where Computer contains honeypot
| where EventID in (4625, 4771, 4776)
| where isnotempty(IpAddress) and IpAddress != "-"
| extend Source = "RDP/SMB", IP = IpAddress),
(W3CIISLog
| where TimeGenerated between (start .. end)
| where Computer contains honeypot
| where tolong(scStatus) >= 400
| extend Source = "IIS", IP = cIP),
(DeviceNetworkEvents
| where TimeGenerated between (start .. end)
| where DeviceName contains honeypot
| where ActionType in ("ConnectionSuccess", "InboundConnectionAccepted")
| extend Source = "Network", IP = RemoteIP)
| where IP != "127.0.0.1" and IP != "::1"
| summarize value = count()
by row = strcat(IP, " (", Source, ")"),
column = format_datetime(bin(TimeGenerated, 1h), "HH:mm")
| project row, column, value
| order by column asc, value desc
When displaying IP-based heatmaps, add enrichment data for click-to-expand threat panels:
Step 1: Extract unique IPs from your query results
Step 2: Enrich IPs using the enrichment script:
python enrich_ips.py 80.94.95.83 193.142.147.209 101.36.107.228
Step 3: Transform enrichment output to heatmap format:
enrichment_out = []
for e in enrichment_data:
threat_cats = []
for c in e.get('recent_comments', [])[:5]:
threat_cats.extend(c.get('categories', []))
enrichment_out.append({
'ip': e['ip'],
'city': e.get('city', 'Unknown'),
'country': e.get('country', '??'),
'org': e.get('org', 'Unknown'),
'is_vpn': e.get('is_vpn') or e.get('vpnapi_security_vpn', False),
'abuse_confidence_score': e.get('abuse_confidence_score', 0),
'total_reports': e.get('total_reports', 0),
'last_reported': e.get('recent_comments', [{}])[0].get('date', '')[:10],
'threat_categories': list(set(threat_cats))[:5]
})
Step 4: Include in heatmap call:
mcp_sentinel-heat_show-signin-heatmap({
"data": [...],
"enrichment": [<enrichment_out>],
...
})
When enrichment is provided:
| Scale | Low Value | High Value | Best For |
|-------|-----------|------------|----------|
| green-red | Teal/Blue | Green | Positive activity (sign-ins, successful ops) |
| blue-red | Blue | Red | Threats/failures (attacks, errors, risks) |
| blue-yellow | Blue | Yellow | Neutral data (general distributions) |
Is the data about threats/failures/attacks?
→ YES: Use "blue-red" (red = danger)
→ NO: Is high volume a positive indicator?
→ YES: Use "green-red" (green = success)
→ NO: Use "blue-yellow" (neutral)
# Query attack data
mcp_sentinel-data_query_lake({
"query": "SecurityEvent | where TimeGenerated between (datetime(<START_DATE>) .. datetime(<END_DATE>)) | where Computer contains '<HONEYPOT_SERVER>' | where EventID == 4625 | where IpAddress != '127.0.0.1' | summarize value = count() by row = IpAddress, column = format_datetime(bin(TimeGenerated, 1h), 'HH:mm') | project row, column, value | order by column asc, value desc | take 200"
})
# Enrich top IPs
python enrich_ips.py 80.94.95.83 193.142.147.209 101.36.107.228
# Display heatmap
mcp_sentinel-heat_show-signin-heatmap({
"data": [
{"row": "80.94.95.83", "column": "19:00", "value": 636},
{"row": "193.142.147.209", "column": "20:00", "value": 245},
...
],
"title": "Honeypot Attack Analysis - Click IP for Threat Intel",
"rowLabel": "Attacker IP",
"colLabel": "Hour (UTC)",
"valueLabel": "Failed Auth Attempts",
"colorScale": "blue-red",
"enrichment": [
{"ip": "80.94.95.83", "city": "Timișoara", "country": "RO", "org": "AS204428 SS-Net", "is_vpn": false, "abuse_confidence_score": 100, "total_reports": 975, "threat_categories": ["RDP Brute-Force", "Hacking"]},
{"ip": "193.142.147.209", "city": "Amsterdam", "country": "NL", "org": "AS213438 ColocaTel Inc.", "is_vpn": true, "abuse_confidence_score": 100, "total_reports": 30972, "threat_categories": ["SSH Brute-Force", "Port Scan"]}
]
})
# Query sign-in data
mcp_sentinel-data_query_lake({
"query": "SigninLogs | where TimeGenerated > ago(24h) | where ResultType == 0 | summarize value = count() by row = AppDisplayName, column = format_datetime(bin(TimeGenerated, 1h), 'HH:mm') | project row, column, value | order by column asc"
})
# Display heatmap (no enrichment needed - not IP-based)
mcp_sentinel-heat_show-signin-heatmap({
"data": [
{"row": "Microsoft Teams", "column": "09:00", "value": 145},
{"row": "Outlook", "column": "09:00", "value": 312},
...
],
"title": "Sign-In Activity by Application (Last 24h)",
"rowLabel": "Application",
"colLabel": "Hour (UTC)",
"valueLabel": "Sign-ins",
"colorScale": "green-red"
})
Problem: The heatmap MCP app sorts columns alphabetically. Labels like Nov 10, Dec 01, Jan 05, Feb 02 will render as Dec → Feb → Jan → Nov — completely out of chronological order.
Solution: Always use ISO date format (YYYY-MM-DD) for time-based column labels. 2025-11-10, 2025-12-01, 2026-01-05 sorts correctly both alphabetically and chronologically.
// ✅ CORRECT — sortable column labels
| summarize value = count() by row = ..., column = format_datetime(bin(TimeGenerated, 7d), "yyyy-MM-dd")
// ❌ WRONG — alphabetic sort breaks chronological order
| summarize value = count() by row = ..., column = format_datetime(bin(TimeGenerated, 7d), "MMM dd")
For hourly heatmaps within a single day, HH:mm is fine (00:00–23:00 sorts correctly). The issue only affects multi-day/week/month labels.
✅ Good Use Cases:
❌ Skip Heatmaps When:
Last Updated: January 29, 2026
development
Use this skill when asked to investigate a computer, device, endpoint, or machine for security issues, suspicious activity, malware, or compliance review. Triggers on keywords like "investigate computer", "investigate device", "investigate endpoint", "check machine", "device security", "endpoint investigation", or when a device name/hostname is mentioned with investigation context. This skill provides comprehensive device security analysis including Defender alerts, sign-in patterns, logged-on users, vulnerabilities, software inventory, compliance status, network activity, and automated investigation tracking for Entra Joined, Hybrid Joined, and Entra Registered devices.
development
Recommended starting point for new users and daily SOC operations. Quick 15-minute security posture scan across 7 domains: active incidents, identity (human + NonHuman), endpoint, email threats, admin & cloud ops, and exposure. 12 queries executed in parallel batches, producing a prioritized Threat Pulse Dashboard with color-coded verdicts (🔴 Escalate / 🟠 Investigate / 🟡 Monitor / ✅ Clear) and drill-down recommendations pointing to specialized skills. Trigger on getting-started questions like "what can you do", "where do I start", "help me investigate". Supports inline chat and markdown file output
development
Use this skill when asked to investigate a user account for security issues, suspicious activity, or compliance review. Triggers on keywords like "investigate user", "security investigation", "user investigation", "check user activity", "analyze sign-ins", or when a UPN/email is mentioned with investigation context. This skill provides comprehensive Entra ID user security analysis including sign-in anomalies, MFA status, device compliance, audit logs, security incidents, Identity Protection risk, and automated reports (HTML, markdown file, or inline chat).
development
Use this skill when asked to generate SVG data visualization dashboards from investigation data or skill reports. Triggers on keywords like "generate SVG dashboard", "create a visual dashboard", "visualize this report", "SVG from the report", "visualize results", "create SVG chart", "SVG from this data". Supports two modes: manifest-driven structured dashboards (from skill reports with svg-widgets.yaml) and freeform adaptive visualizations from ad-hoc investigation data. Component library includes KPI cards, score cards, bar charts, line charts, donut charts, waterfall charts, tables, recommendation cards, assessment banners. SharePoint Dark Theme default palette.