Use this skill when asked to investigate a security incident by ID from Microsoft Defender XDR or Microsoft Sentinel...
This skill performs comprehensive security investigations on incidents from Microsoft Defender XDR and Microsoft Sentinel. It retrieves incident details, lists alerts, enumerates assets and evidences, and then performs deep investigation on user-selected entities using appropriate tools and specialized skills.
Investigation Flow:
Before starting ANY incident investigation:
list_sentinel_workspaces MCP tool BEFORE presenting the investigation menu.github/skills/user-investigation/SKILL.md.github/skills/computer-investigation/SKILL.md.github/skills/ioc-investigation/SKILL.mdhxxps://evil[.]com, 203[.]0[.]113[.]42Incident ID Patterns:
| Pattern | Source | Tool to Use |
|---|---|---|
Numeric (e.g., 12345, 98765) |
Defender XDR / Sentinel | GetIncidentById |
| GUID format | Sentinel (internal) | Sentinel query_lake MCP tool |
INxx-xxxxx format |
Defender XDR | GetIncidentById |
โ ๏ธ Sentinel โ Defender XDR ID Mapping (Critical):
When an incident is discovered via Sentinel KQL (e.g., SecurityIncident or SecurityAlert tables), its IDs are Sentinel-local and will NOT work with the Triage MCP:
| Sentinel Field | Triage MCP Accepts? | Correct Field to Use |
|---|---|---|
SecurityIncident.IncidentNumber |
โ Returns "not found" | Use SecurityIncident.ProviderIncidentId |
SecurityAlert.SystemAlertId |
โ Returns "not found" | Extract parse_json(ExtendedProperties).IncidentId |
SecurityIncident.ProviderIncidentId |
โ | Pass directly to GetIncidentById |
Rule: When querying SecurityIncident for later Triage MCP drill-down, always project ProviderIncidentId alongside IncidentNumber. Use ProviderIncidentId for all GetIncidentById calls.
Date Range Rules:
2026-01-17T00:00:00Z to 2026-01-24T00:00:00Z)Confirm whether an IP appears across many unrelated accounts in the window. This is the empirical test behind Critical Rule 8.
let TargetIPs = dynamic(["<ip1>","<ip2>"]);
EntraIdSignInEvents
| where Timestamp between (datetime(<start>) .. datetime(<end>))
| where IPAddress in (TargetIPs)
| summarize DistinctAccounts = dcount(AccountUpn), SignIns = count(),
Days = dcount(bin(Timestamp, 1d)), SampleAccounts = make_set(AccountUpn, 5)
by IPAddress
| order by DistinctAccounts desc
| Result | Interpretation |
|---|---|
High DistinctAccounts across many Days |
Shared infrastructure โ proven. Overrides a Defender roles: ["attacker"] tag and beats an ASN lookup or a 0% AbuseIPDB score, which only describe the IP's owner, not its use in this tenant |
DistinctAccounts == 1 |
Singleton โ NOT shared infrastructure. The "corporate egress / shared jump host / VPN pool" benign hypothesis is eliminated, not unresolved (Gate 2) |
| No row returned | The IP was never seen in sign-in telemetry. Find which source did observe it (AlertEvidence, CloudAppEvents) before citing it as a session IP |
Feed each result into Gate 1 โ an entity whose baseline is mostly singleton IPs behaves differently from one that lives on shared infrastructure.
Report only the counts this query returned. Never supplement, substitute, or "remember" which accounts share an IP โ a co-occurrence recalled from cached context is a hypothesis, and writing it into a column labelled as query output converts it into false evidence.
This phase retrieves and presents all incident information. Follow the exact structure below.
Retrieve and list the incident's metadata using GetIncidentById:
| Field | Description |
|---|---|
| Title | Incident display name |
| Description | Detailed incident description |
| Status | Active, Resolved, Redirected |
| Severity | High, Medium, Low, Informational |
| Priority assessment | If available from incident data |
| Classification | TruePositive, FalsePositive, BenignPositive, etc. |
| Determination | Malware, Phishing, etc. |
| Created Date | When incident was created |
| First Activity Date | First malicious activity timestamp |
| Last Updated Date | Most recent modification |
| Assigned To | Analyst assigned to incident |
| MITRE Categories | Tactics and techniques involved |
| Tags | Labels applied to incident |
Use GetIncidentById with includeAlertsData=true to retrieve incident-specific alerts. This returns only alerts correlated to the incident.
โ DO NOT use ListAlerts to retrieve alerts for a specific incident. ListAlerts has NO incidentId parameter โ it can only filter by createdAfter, createdBefore, severity, status. Calling it returns all tenant alerts (up to page size 10,000), not incident-specific ones. Any unsupported parameter (e.g., incidentId) is silently ignored.
If GetIncidentById(includeAlertsData=true) returns a truncated or excessively large response (e.g., incident has hundreds of correlated alerts from noise sources like Purview IRM or DLP), use RunAdvancedHuntingQuery as the fallback:
// Get alerts linked to the incident's primary user/entity
AlertInfo
| where Timestamp > datetime(<incident_created_minus_7d>)
| join kind=inner (
AlertEvidence
| where Timestamp > datetime(<incident_created_minus_7d>)
| where EntityType == "User"
| where AccountUpn =~ "<primary_user_upn>" or AccountObjectId == "<user_object_id>"
| distinct AlertId
) on AlertId
| project Timestamp, AlertId, Title, Severity, Category, AttackTechniques, DetectionSource, ServiceSource
| order by Timestamp asc
This approach bypasses the Triage MCP's alert cap and gives full control over date range and entity filtering.
For each alert, retrieve:
๐ด Surface pre-existing determinations. Classification and Determination are often already populated by a prior analyst or by automated investigation. Show them in the alert table and treat them as evidence โ an alert already marked notMalicious materially changes the weight of that alert. Do not silently re-litigate a closed determination; either corroborate it or state explicitly why you disagree.
Presentation Rules:
Disposition column and fill it in once Phase 2 completes. Every alert must exit the investigation with exactly one of:| Disposition | Meaning |
|---|---|
Supports verdict |
Investigated; corroborates the cluster verdict |
Ruled out โ <reason> |
Investigated; explained by a benign cause. State the reason inline |
Not investigated |
Outside the selected scope. Must also appear in Phase 3's remaining-entities list |
No alert may be silently dropped. An alert missing from the final table is an unaccounted-for detection.
Retrieve and list ALL assets involved in the incident by type:
Device Assets:
| Field | Description |
|---|---|
| Name | Device hostname |
| Domain | AD domain |
| Risk Level | Device risk assessment |
| Exposure Level | Vulnerability exposure |
| OS Platform | Operating system |
User Assets:
| Field | Description |
|---|---|
| Display Name | User's full name |
| UPN | User Principal Name |
| User Status | Account status |
| Domain | User's domain |
| Department | Organizational department |
App Assets:
| Field | Description |
|---|---|
| App Name | Application name |
| App Client ID | OAuth client ID |
| Risk | Application risk level |
| Publisher | App publisher |
Cloud Resource Assets:
| Field | Description |
|---|---|
| Resource Name | Cloud resource identifier |
| Status | Resource status |
| Cloud Environment | Azure, AWS, GCP, etc. |
| Type | Resource type |
Count assets by type ONLY after retrieving complete lists.
Retrieve evidences classified as malicious or suspicious only:
Processes (Top 10):
Files (Top 10):
IP Addresses (Top 10, Filtered):
203[.]0[.]113[.]42, evil[.]com. NEVER output clickable malicious indicators.URLs and DNS Domains (Top 10, Filtered):
hxxps://evil[.]com/path, hxxp://malware[.]net. NEVER output clickable malicious URLs.AD Domains:
For each evidence type: If more than 10 exist, note this after the table and provide Defender portal link.
Incident correlation is fallible in both directions: it can fuse unrelated activity into one incident, and it can leave related alerts outside it. Run both checks before Phase 2 so the investigation scope is correct from the start.
Group the incident's alerts by shared entity (user, device, IP, session/correlation ID) and present the clusters:
| Cluster | Alerts | Shared Entities | Time Span |
|---|---|---|---|
| A | 4 | user1@contoso.com, 203[.]0[.]113[.]42 |
2026-01-20 04:07 โ 04:15 |
| B | 7 | user2@contoso.com, 198[.]51[.]100[.]10 |
2026-01-14 08:00 โ 08:01 |
๐ด De-correlated โ dismissed. A cluster found not to relate to the seed alert is never dropped โ it either receives its own verdict in this report or is explicitly handed off as a new investigation. Report the correlation defect itself as a finding: it is actionable detection-engineering feedback regardless of whether the underlying activity was malicious.
The incident's alert list is not the full picture of what its entities did. Sweep for alerts that touch an incident entity but were not correlated into the incident:
let IncidentAlertIds = dynamic(["<alertId1>", "<alertId2>"]); // from GetIncidentById
let IncidentEntities = dynamic(["user1@contoso.com", "203.0.113.42", "HOST-01"]);
let IncidentObjectIds = dynamic(["<userObjectId1>"]); // more reliable than UPN
let WindowStart = datetime(<incident_created_minus_7d>);
let WindowEnd = datetime(<incident_last_activity_plus_1d>);
AlertEvidence
| where Timestamp between (WindowStart .. WindowEnd)
| where AccountUpn in~ (IncidentEntities)
or AccountObjectId in (IncidentObjectIds)
or RemoteIP in (IncidentEntities)
or DeviceName in~ (IncidentEntities)
| where AlertId !in (IncidentAlertIds)
| distinct AlertId
| join kind=inner (
AlertInfo
| where Timestamp between (WindowStart .. WindowEnd)
) on AlertId
| project Timestamp, Title, Severity, Category, DetectionSource, ServiceSource, AttackTechniques
| order by Timestamp asc
Tool: RunAdvancedHuntingQuery โ the default 7d window is well within the 30d AH limit. For windows >30d use mcp_sentinel-data_query_lake and swap Timestamp โ TimeGenerated.
โ Mandatory โ no substitutes. Prior investigations, cached context, or a high-confidence early verdict tell you how to interpret what this sweep returns; none of them tell you what it would have returned. Run it every time and state the row count, including zero.
Report either outcome:
This checkpoint MUST be completed before ANY entity investigation can proceed.
ALWAYS execute this step first, regardless of any other considerations:
list_sentinel_workspaces (MCP tool)
Store the result. This determines the workflow for Step 2.3.
Show a summary of the incident entities and assets from Phase 1:
๐ด Defang every URL, domain, and IP (Critical Rule 9) โ hxxps://evil[.]com/path, evil[.]com, 203[.]0[.]113[.]42. Never output clickable malicious indicators.
IF workspace_count == 1:
- Auto-select the single workspace
- Display: "Using Sentinel workspace: [NAME] ([ID])"
- Set SESSION_WORKSPACE_SELECTED = true
ELSE IF workspace_count > 1 AND SESSION_WORKSPACE_SELECTED == false:
- Display all workspaces with Name and ID
- ASK USER: "Which Sentinel workspace should I run my searches in? Select one or more, or choose 'all'."
- WAIT for user response
- Set SESSION_WORKSPACE_SELECTED = true after selection
ELSE IF workspace_count > 1 AND SESSION_WORKSPACE_SELECTED == true:
- Display: "Continuing with previously selected workspace: [NAME] ([ID])"
- DO NOT ask again
If SESSION_WORKSPACE_SELECTED == false after Step 2.3, STOP and ask the user to select a workspace.
Ask the user:
"Which assets and entities involved in the incident should be investigated in depth? Please select them by providing their numbers or names, or simply ask to analyze all of them. The more entities you select, the longer the analysis will take."
๐ด DO NOT OFFER OTHER OPTIONS: Only ask the user whether they want to investigate one or more of the incident entities and assets listed above in more depth.
Read the response.
Pre-flight check: Confirm SESSION_WORKSPACE_SELECTED == true before proceeding.
Proceed in accordance with the instructions described below for Phase 2-A, Phase 2-B, and Phase 2-C. When multiple investigation types are selected (users, devices, IoCs) run them in parallel as much as possible.
Per-event detail is what makes a verdict defensible and a cluster separation provable. For every event cited as evidence, capture:
| Telemetry | Capture |
|---|---|
| Sign-in | CorrelationId/RequestId, SessionId, ResultType/ErrorCode, RiskLevelDuringSignIn, RiskEventTypes, device managed + compliant state and DeviceId, user agent, ClientAppUsed, AuthenticationRequirement |
| Graph / API | Operation, HTTP method, ResponseStatusCode, response size where the platform exposes it (MicrosoftGraphActivityLogs.ResponseSizeBytes), request URI, calling app/SPN identity |
| Cloud app | ActionType, AccountObjectId, IPAddress, user agent, and the specific RawEventData fields cited (parse once into a variable โ never tostring(RawEventData) has ...) |
| Device / process | DeviceId, initiating-process chain, SHA256, signer/publisher, FolderPath, command line |
Why these specifically:
๐ด Validate the central artefact at its source. When an alert's premise rests on a specific artefact, query the authoritative table for that artefact directly. Never infer its nature from the alert title.
| Artefact | Authoritative source | Report verbatim |
|---|---|---|
EmailEvents by NetworkMessageId |
ThreatTypes, DetectionMethods, DeliveryAction, DeliveryLocation, EmailDirection, AuthenticationDetails |
|
| URL click | UrlClickEvents by Url / NetworkMessageId |
ActionType, IsClickedThrough, UrlChain |
| File | DeviceFileEvents or GetDefenderFileInfo by hash |
GlobalPrevalence, signer, FirstSeen |
| Sign-in | SigninLogs / EntraIdSignInEvents by CorrelationId |
ResultType, RiskLevelDuringSignIn, CA outcome |
The platform's own disposition outranks the alert title. An alert named "phish delivered" whose EmailEvents row shows empty ThreatTypes, empty DetectionMethods, and DeliveryAction: Delivered was not a detected phish. Say so, and record the discrepancy itself as a finding.
โ VERIFY BEFORE PROCEEDING:
SESSION_WORKSPACE_SELECTED == true (workspace explicitly selected by user)SELECTED_WORKSPACE_IDS array is populated with user's selectionIf any pre-requisite is FALSE: STOP and return to Phase 2.3 Workspace Selection Gate.
โก PARALLEL EXECUTION: When multiple users are selected, execute user investigations in parallel as much as possible.
๐ฆ WORKSPACE CONTEXT: Pass the selected workspace(s) to all child skill invocations:
SELECTED_WORKSPACE_IDS from Phase 2.3 for all Sentinel queriesFor EACH user selected by the user:
๐ด REFERENCE THE SKILL FILE: Read and follow the complete workflow defined in:
.github/skills/user-investigation/SKILL.md
Key Steps (summary - see skill file for full details):
DO NOT copy the full workflow here - always read the skill file for the most current instructions.
โก PARALLEL EXECUTION: When multiple devices are selected, execute device data collection queries in parallel for ALL devices simultaneously. Run Defender alerts, compliance, logged-on users, vulnerabilities, network/process/file events queries concurrently.
For EACH device selected by the user:
๐ด REFERENCE THE SKILL FILE: Read and follow the complete workflow defined in:
.github/skills/computer-investigation/SKILL.md
Key Steps (summary - see skill file for full details):
DO NOT copy the full workflow here - always read the skill file for the most current instructions.
โก PARALLEL EXECUTION: When multiple IoCs are selected, execute ALL IoC investigation queries in parallel. Run threat intel lookups, Sentinel queries, and organizational exposure queries concurrently for all IoCs.
For EACH IoC selected by the user:
๐ด REFERENCE THE SKILL FILE: Read and follow the complete workflow defined in:
.github/skills/ioc-investigation/SKILL.md
Supported IoC Types:
| IoC Type | Detection Pattern | Key Investigation Points |
|---|---|---|
| URL | https?:// or domain pattern |
Malicious indicators, phishing, threat intel, organizational exposure |
| IPv4 Address | \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} |
Threat intel, network connections, geographic analysis |
| IPv6 Address | Contains multiple colons | Same as IPv4 |
| Domain | [a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z]{2,} |
DNS queries, email threats, reputation |
| MD5 Hash | 32 hex characters | File prevalence, malware analysis |
| SHA1 Hash | 40 hex characters | File prevalence, malware analysis |
| SHA256 Hash | 64 hex characters | File prevalence, malware analysis |
Key Steps (summary - see skill file for full details):
DO NOT copy the full workflow here - always read the skill file for the most current instructions.
After completing ALL selected entity investigations in Phase 2, first check whether the loop is needed at all:
| Condition | Action |
|---|---|
| Material entities remain uninvestigated | Run the loop below |
| No material entities remain โ all investigated, or the remainder were filtered out in ยง1.4 as internal/tenant-owned | โญ๏ธ Skip to Phase 4. Never present an empty "remaining entities" table |
| User requested a complete / one-shot investigation (e.g. "investigate incident X and write the report", "investigate everything") | โญ๏ธ Skip to Phase 4. The loop exists to confirm scope, not to withhold the verdict |
When the loop IS needed, you MUST:
โ Step 3.1: Compile list of UNINVESTIGATED entities (exclude already-investigated items)
โ Step 3.2: Present remaining entities to user with numbered list
โ Step 3.3: Ask: "Would you like to investigate any of the remaining entities? Select by number/name, or say 'done' to conclude."
โ Step 3.4: Wait for user response before concluding
When the loop is needed, end the findings with:
๐ Remaining Uninvestigated Entities:
# Type Entity Notes 1 Device [DEVICE_NAME] [Risk level or relevant context] 2 File [FILENAME] [Hash or detection status] 3 URL [DEFANGED_URL] [Threat assessment] ... ... ... ... Would you like to investigate any of these remaining entities? Select by number/name, type "all" to investigate everything, or say "done" to conclude the investigation.
IF no material entities remain
OR a complete / one-shot investigation was requested:
โ Proceed to Phase 4 directly โ do NOT present the list, do NOT ask first
ELSE present the remaining-entities list, then:
IF user selects additional entities:
โ Return to Phase 2 (2-A, 2-B, or 2-C based on entity type)
โ After completion, return to Phase 3 again
ELSE IF user says "done" or declines:
โ Proceed to Phase 4 (Final Summary & Verdict Gates)
Entry condition โ reached by ANY of the following:
๐ด Phase 4 is never optional and never waits for permission. Any output that states a classification โ inline in chat, in a report file, or in a recorded finding โ must have passed these gates first.
For every entity you are about to call anomalous, unusual, or compromised, pull 14โ30 days of prior activity and diff it against the flagged activity across: application set, source IPs/geography, operation mix, volume, and success/failure ratio.
State the baseline explicitly in the report โ including when it is absent (new account, no prior telemetry, retention exceeded).
๐ก A source IP that is new to an entity is novelty. A source IP that is new and carries operations absent from the baseline and breaks the normal volume envelope is anomaly. Only the second justifies escalation.
Diff the pattern, not only the value. When a value is new, also ask how often this entity acquires new values of that attribute. If the baseline shows a recurring acquisition rate โ a fresh short-lived source IP every 7โ10 days, a new device each month โ then the next new value is baseline-consistent, not novel. A correct value-level diff still yields a wrong verdict when the rate of change is itself the norm. Classify each baseline value by prevalence (Rule 8) so the pattern is described accurately.
| Action | Status |
|---|---|
| Calling behaviour "reconnaissance", "anomalous", "unusual", or "malicious" with no stated baseline | โ PROHIBITED |
| Labelling a value "novel" without checking how often the entity acquires new values of that attribute | โ PROHIBITED |
| A baseline diff before any behavioural verdict | โ REQUIRED |
State at least one competing benign explanation and resolve it with evidence:
| Hypothesis | Supporting Evidence | Contradicting Evidence | Verdict |
|---|---|---|---|
| Automation / service workload | Same user agent present on baseline activity | โ | โ Accepted |
| Credential compromise | Source IP new to this entity | 0 auth failures; compliant managed device; no risk detection | โ Rejected |
Include this table even when the malicious hypothesis wins โ it demonstrates the alternative was tested rather than ignored.
Words like "attacker", "threat actor", "adversary", "malicious", and "compromised" require at least one of:
Unfamiliarity is not attribution. A new IP, a new user agent, or a first-seen app is novelty, not adversary presence.
Attribution confidence must never exceed overall verdict confidence. If the verdict is "inconclusive", the infrastructure cannot be described as "attacker-controlled".
Explicitly enumerate what was searched for and not found. Silence is not evidence of absence โ state what you checked:
Also state telemetry gaps โ tables unavailable, licences absent, retention exceeded โ so a reader can distinguish "checked and clean" from "could not check".
Emit one block per cluster identified in ยง1.5 โ not one blended verdict for the whole incident.
Use plain headings and bullets, matching the rest of the report. Do not wrap verdict blocks in blockquotes or code fences โ Gate 2's hypothesis table and any supporting evidence tables must nest beneath the block without a > prefix on every row.
#### Cluster A โ `<entity>` ยท `<verdict>` ๐ข / ๐ก / ๐ / ๐ด
- **Classification:** TruePositive / BenignPositive / FalsePositive / Inconclusive
- **Confidence:** High / Medium / Low
- **Baseline:** *one line โ what normal looks like for this entity*
- **Decisive evidence:** *1โ3 bullets*
- **Not found:** *negative-findings summary*
- **Recommendation:** *action, or "no action required"*
After the last cluster block, close with an #### Overall Assessment heading in the same idiom โ not a loose bold line โ carrying the cross-cluster risk statement. Then provide consolidated recommendations across clusters and offer the JSON export.
Purpose: Starts asynchronous security analysis of a user entity.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId |
string | Yes | User's Azure AD Object ID (GUID) |
startTime |
string | Yes | ISO 8601 format start time |
endTime |
string | Yes | ISO 8601 format end time |
workspaceId |
string | No | Sentinel workspace GUID (optional if only one workspace) |
Time Window Options: 30 days (Comprehensive), 7 days (Standard), 1 day (Quick)
Returns: 202 Accepted with analysisId
Purpose: Retrieves results of an asynchronous entity analysis.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
analysisId |
string | Yes | Analysis ID returned from analyze_*_entity |
Returns: 200 OK with analysis results when complete, or status if still processing
When a user requests an incident investigation:
Phase 1 - Incident Description:
GetIncidentByIdClassification / Determination)Phase 1.5 - Alert Cohesion & Adjacent Sweep:
โ Phase 2 - Mandatory Workspace Selection:
list_sentinel_workspaces MCP tool FIRSTPhase 2-A - User Investigation:
.github/skills/user-investigation/SKILL.mdPhase 2-B - Device Investigation:
.github/skills/computer-investigation/SKILL.mdPhase 2-C - IoC Investigation:
.github/skills/ioc-investigation/SKILL.mdPhase 3 - Post-Investigation Loop:
Phase 4 - Final Summary & Verdict Gates:
| Field | Type | Description |
|---|---|---|
investigation_metadata |
object | Incident ID, timestamp, phases completed, elapsed time |
incident_details |
object | Metadata, alerts (each with disposition), assets, evidences from Phase 1 |
clusters |
array | Phase 1.5 output โ one entry per cohesion cluster, with member alert IDs, shared entities, and separation evidence |
user_investigations |
array | Results from Phase 2-A (includes the stated baseline) |
device_investigations |
array | Results from Phase 2-B |
ioc_investigations |
array | Results from Phase 2-C (IPs, URLs, files, domains, hashes) |
verdicts |
array | Phase 4 โ one verdict block per cluster, each carrying its four gate results |
summary |
object | Overall risk assessment, consolidated recommendations, negative findings |
{
"investigation_metadata": {
"incident_id": "<INCIDENT_ID>",
"investigation_timestamp": "<ISO_TIMESTAMP>",
"phases_completed": ["incident_description", "cohesion_check", "user_investigation", "ioc_investigation", "verdict"],
"total_elapsed_time_seconds": 300
},
"incident_details": {
"metadata": {
"title": "<TITLE>", "severity": "<SEVERITY>", "status": "<STATUS>",
"classification": "<CLASSIFICATION>", "determination": "<DETERMINATION>",
"created_date": "<TIMESTAMP>", "first_activity_date": "<TIMESTAMP>",
"mitre_categories": ["<TACTIC>"], "tags": ["<TAG>"]
},
"alerts": [
{
"id": "<ALERT_ID>", "name": "<ALERT_NAME>", "severity": "<SEVERITY>",
"classification": "<PRE_EXISTING_CLASSIFICATION>", "determination": "<PRE_EXISTING_DETERMINATION>",
"first_activity": "<TIMESTAMP>", "last_activity": "<TIMESTAMP>",
"cluster": "A",
"disposition": "Ruled out โ mailbox is a lab honeypot; no click, no credential entry"
}
],
"assets": { "devices": [], "users": [], "apps": [], "cloud_resources": [] },
"evidences": { "processes": [], "files": [], "ip_addresses": [], "urls": [], "ad_domains": [] }
},
"clusters": [
{
"id": "A", "label": "<SHORT_LABEL>", "alert_ids": ["<ALERT_ID>"],
"shared_entities": ["user@contoso.com"],
"time_span": { "start": "<TIMESTAMP>", "end": "<TIMESTAMP>" },
"linked_to_other_clusters": false,
"separation_evidence": "No shared UPN, IP, DeviceId, or SessionId with cluster B; 6 days apart",
"sweep_performed": true,
"uncorrelated_adjacent_alerts": 3
}
],
"user_investigations": [
{
"upn": "user@contoso.com", "user_id": "<GUID>",
"time_window": { "start": "<ISO_TIMESTAMP>", "end": "<ISO_TIMESTAMP>" },
"baseline": {
"window_days": 30, "typical_ips": [], "typical_countries": [],
"typical_apps": [], "typical_devices": []
},
"findings": {}, "risk_level": "Low"
}
],
"device_investigations": [
{ "hostname": "<DEVICE_NAME>", "device_id": "<GUID>", "findings": {} }
],
"ioc_investigations": [
{
"ioc_type": "IP", "value": "203.0.113.42",
"prevalence": { "distinct_accounts": 553, "days": 30 },
"threat_assessment": "Shared infrastructure โ not attacker-controlled",
"findings": {}
}
],
"verdicts": [
{
"cluster": "A",
"classification": "BenignPositive",
"determination": "SecurityTesting",
"confidence": "High",
"gates": {
"baseline": "30-day baseline established; all observed IPs and apps fall within it",
"alternatives_considered": [
{
"hypothesis": "Credential theft via phish",
"status": "Eliminated",
"evidence": "No click in UrlClickEvents; no sign-in from sender infrastructure"
}
],
"attribution_evidence": "EmailEvents shows empty ThreatTypes and DetectionMethods โ no platform detection backs the alert title",
"negative_findings": ["No mailbox rule creation", "No OAuth consent grant", "No sign-in risk detection"]
},
"recommendations": []
}
],
"summary": {
"risk_assessment": "Low",
"key_findings": [],
"negative_findings": [],
"recommendations": []
}
}
| Issue | Solution |
|---|---|
| Incident not found | Verify incident ID format; try Sentinel query if Defender fails |
| User Object ID not found | Verify UPN is correct; check if user exists in Entra ID |
| analyze_user_entity returns error | Check userId is GUID format; verify time window โค 30 days |
| get_entity_analysis still processing | Poll again after 5-10 seconds; max 2 minutes |
| No workspace found | Use list_sentinel_workspaces MCP tool to get workspace ID |
| Device investigation fails | Verify device exists in Defender; check device ID type |
| IoC investigation timeout | Reduce date range; check IoC format |
See Phase 2 Step 2.3. Do not restate or re-derive the rule โ and never re-ask once SESSION_WORKSPACE_SELECTED == true.
User Request: "Investigate incident 12345"
โ ๏ธ This example is deliberately abbreviated. It exists to show the shape of a compliant investigation โ specifically the Phase 1.5 clustering table and the Phase 4 verdict blocks. Do not copy findings or phrasing from it.
Retrieve metadata via GetIncidentById, then present alerts, assets, and filtered evidences per ยง1.1โยง1.4. The alert table carries Classification, Determination, and the Disposition column (completed at the end of Phase 2).
[01:30] Cohesion check โ 7 alerts
| Cluster | Alerts | Shared entities | Time span |
|---------|--------|-------------------------------------|-----------------------------|
| A | 5 | jsmith@contoso.com, WORKSTATION-01 | Jan 20 10:30 โ Jan 20 14:05 |
| B | 2 | svc-backup@contoso.com | Jan 14 03:12 โ Jan 14 03:14 |
Separation evidence: no shared UPN, IP, DeviceId, or SessionId between A and B; 6 days apart.
โ Investigate and verdict A and B SEPARATELY. De-correlated โ dismissed.
Sweep: 3 further alerts on cluster-A entities in the ยฑ7d window were NOT correlated into
the incident (2 ร Purview DLP, 1 ร sign-in risk). Both clusters' entity sets swept.
Workspace gate โ entity selection โ run 2-A / 2-B / 2-C in parallel, applying the Evidence Capture Standard to every cited event. Validate central artefacts at source: EmailEvents by NetworkMessageId, hash prevalence via GetDefenderFileInfo, and IPs via the IP Prevalence Check.
A cited event carries its identifiers, not just its label โ e.g. "GET /v1.0/me/messages?$search=โฆ โ 200, ResponseSizeBytes 1,204,880, 312 calls in 4 min by app <AppDisplayName>", not "mail enumeration detected". The same rule governs the negative: "no exfiltration found" requires showing the operations you looked at, otherwise it is an assumption.
Two entities remained uninvestigated (SERVER-DC01, jdoe@contoso.com); neither was material to either cluster, so the loop was skipped and the reason stated. Both are recorded with disposition: "Not investigated".
โโโ Cluster A โ WORKSTATION-01 / jsmith@contoso.com โโโ
Classification: TruePositive Determination: Malware Confidence: High
Gate 1 โ Baseline (30d): jsmith signs in from 2 IPs, both AS12345 corporate egress,
always from WORKSTATION-01, always compliant + managed. The Jan 20 session came from a
singleton IP (DistinctAccounts = 1), an unmanaged device, ClientAppUsed = Other.
Gate 2 โ Alternatives: VPN/travel ELIMINATED (no travel record; device unmanaged).
Shared egress ELIMINATED (prevalence = 1 account). Admin tooling ELIMINATED (no PIM activation).
Gate 3 โ Attribution: SHA256 abc123โฆ GlobalPrevalence = 4, unsigned, written to %TEMP%;
DeviceNetworkEvents shows 42 outbound beacons to 203[.]0[.]113[.]42 at 60s intervals.
Gate 4 โ Negative findings: no lateral movement; no LSASS access events; no mailbox rule
creation; no OAuth consent grant.
โโโ Cluster B โ svc-backup@contoso.com โโโ
Classification: BenignPositive Determination: SecurityTesting Confidence: High
Gate 1 โ Baseline (30d): identical ~3-minute burst at 03:12 every weekday, same IP, same app.
Gate 2 โ Alternatives: compromise ELIMINATED (100% success ratio, zero auth failures,
no off-schedule activity across the full 30d window).
Gate 3 โ Attribution: scheduled backup job; user agent and app ID match documented automation.
Gate 4 โ Negative findings: no scope expansion; no new credentials added to the SPN.
Consolidated recommendations follow the verdict blocks, each tagged with its cluster and severity.
Export: temp/incident_investigation_12345_<date>.json
This skill orchestrates investigations by referencing specialized skills:
| Investigation Phase | Skill/Tool | Location/Reference |
|---|---|---|
| Phase 1: Incident Description | Built-in workflow | This file (see Phase 1 section) |
| Phase 1.5: Alert Cohesion & Sweep | Built-in workflow | This file (see ยง1.5) |
| Phase 2-A: User Investigation | user-investigation skill | .github/skills/user-investigation/SKILL.md |
| Phase 2-B: Device Investigation | computer-investigation skill | .github/skills/computer-investigation/SKILL.md |
| Phase 2-C: IoC Investigation | ioc-investigation skill | .github/skills/ioc-investigation/SKILL.md (IPs, URLs, Files, Domains, Hashes) |
| Phase 4: Verdict Gates | Built-in workflow | This file (see Phase 4 section) |
๐ด ALWAYS read the referenced skill file before executing that phase to ensure proper workflow execution.