kql-writing
Skill35 starsChanged 46 days agoMITView on GitHub
Use when the user asks for a KQL query, a Microsoft Sentinel / Defender / Azure Log Analytics detection or hunt, or wants to translate a finding from `/hash-investigation` / `/malware-analysis` into KQL. Format spec + writing guide.
In this repository79 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- abuseipdb-apiSkill
- achSkill
- campaign-trackingSkill
- carding-financial-fraudSkill
- censys-apiSkill
- china-cyber-espionageSkill
- claim-extractionSkill
- confidence-levelsSkill
- control-coverage-mappingSkill
- crowdstrike-apiSkill
- cti-hyperloopSkill
- cti-orchestratorSkill
- cti-setupSkill
- darkweb-collectionSkill
- devils-advocacySkill
- domain-investigationSkill
- dprk-cyber-espionageSkill
- feedback-loopsSkill
- greynoise-apiSkill
- hacktivismSkill
- hash-investigationSkill
- horizon-scanningSkill
- indicator-pivotingSkill
- infostealersSkill
- initial-access-brokersSkill
- intelligence-sharingSkill
- intelligence-writingSkill
- ioc-enrichment-workflowSkill
- ioc-exportSkill
- ip-investigationSkill
- iran-cyber-espionageSkill
- key-assumptions-checkSkill
- likelihood-languageSkill
- lookup-abuseipdbSkill
- lookup-censysSkill
- lookup-crowdstrikeSkill
- lookup-greynoiseSkill
- lookup-liberty91Skill
- lookup-mispSkill
- lookup-openctiSkill
- lookup-otxSkill
- lookup-ransomwareliveSkill
- lookup-reversinglabsSkill
- lookup-sentinelSkill
- lookup-shodanSkill
- lookup-urlscanSkill
- lookup-virustotalSkill
- malware-analysisSkill
- maturity-assessmentSkill
- mitre-attackSkill
- osint-methodologySkill
- otx-apiSkill
- phishing-social-engineeringSkill
- pir-managementSkill
- quality-controlSkill
- quality-of-information-checkSkill
- ransomware-ecosystemSkill
- reversinglabs-apiSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 2 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 138
Always include a time filter (ago()) to limit query scope
- Very specific: line 139
Use has instead of contains when searching for whole words (faster)
- Very specific: line 140
Prefer in~ over multiple or conditions for case-insensitive list matching
- Very specific: line 141
Use let statements for IOC lists to keep queries readable
- Very specific: line 142
Add project to select only needed columns (reduces result size)
- Distinctive: line 143
Test queries on small time windows first before expanding
- Distinctive: line 151
A query that runs is not a query that saw: When a fallback table has weaker fidelity (or the preferred table is absent entirely), state …
- Very specific: line 157
Write KQL queries to: data/detection-rules/kql/<technique-id>-<slug>.kql
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
KQL Writing Guide — Microsoft Sentinel
KQL (Kusto Query Language) is used in Microsoft Sentinel, Microsoft Defender, and Azure Data Explorer for querying security logs and building detection rules.
Core Syntax
Table references
SecurityEvent // Windows Security Events
DeviceProcessEvents // Defender for Endpoint
DeviceNetworkEvents // Network connections
DeviceFileEvents // File operations
EmailEvents // Defender for Office 365
SigninLogs // Azure AD sign-ins
AuditLogs // Azure AD audit
CommonSecurityLog // CEF/Syslog
ThreatIntelligenceIndicator // TI feed indicatorsOperators
| where TimeGenerated > ago(24h) // Time filter
| where EventID == 4688 // Exact match
| where ProcessCommandLine contains "-enc" // Substring
| where ProcessCommandLine matches regex @".*-e(nc)?.*" // Regex
| where SourceIP !in ("10.0.0.1", "10.0.0.2") // Not in list
| where isnotempty(AccountName) // Not null/empty
| extend NewColumn = extract(@"pattern", 1, SourceField) // Extract
| project TimeGenerated, Account, Computer // Select columns
| summarize count() by bin(TimeGenerated, 1h), Account // Aggregate
| sort by TimeGenerated desc // Sort
| take 100 // Limit results
| join kind=inner (OtherTable) on CommonField // JoinString operators
| Operator | Description | Case-sensitive |
== | Exact match | Yes |
=~ | Exact match | No |
contains | Substring | No |
contains_cs | Substring | Yes |
startswith | Starts with | No |
endswith | Ends with | No |
matches regex | Regex match | Yes |
has | Word boundary match | No |
in | In list | Yes |
in~ | In list | No |
Common Detection Patterns
Suspicious process execution (T1059)
DeviceProcessEvents
| where TimeGenerated > ago(24h)
| where FileName in~ ("powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe")
| where ProcessCommandLine contains_cs "-enc"
or ProcessCommandLine contains "bypass"
or ProcessCommandLine contains "downloadstring"
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileNameSuspicious parent-child (T1566.001)
DeviceProcessEvents
| where TimeGenerated > ago(24h)
| where InitiatingProcessFileName in~ ("outlook.exe", "winword.exe", "excel.exe", "powerpnt.exe")
| where FileName in~ ("cmd.exe", "powershell.exe", "wscript.exe", "mshta.exe", "certutil.exe")
| project TimeGenerated, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLineOutbound connection to IOC (C2)
let IOC_IPs = dynamic(["203.0.113.42", "198.51.100.10"]);
let IOC_Domains = dynamic(["evil.example.com", "c2.badactor.net"]);
DeviceNetworkEvents
| where TimeGenerated > ago(7d)
| where RemoteIP in (IOC_IPs) or RemoteUrl has_any (IOC_Domains)
| project TimeGenerated, DeviceName, RemoteIP, RemoteUrl, RemotePort, InitiatingProcessFileNameFailed sign-in brute force (T1110)
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != "0"
| summarize FailedAttempts = count(), DistinctAccounts = dcount(UserPrincipalName)
by IPAddress, bin(TimeGenerated, 15m)
| where FailedAttempts > 10
| sort by FailedAttempts descLateral movement — PsExec (T1570)
DeviceProcessEvents
| where TimeGenerated > ago(24h)
| where FileName =~ "psexesvc.exe"
or (FileName =~ "cmd.exe" and ProcessCommandLine contains "\\\\")
or ProcessCommandLine contains "-accepteula -s cmd"
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLineDNS query for suspicious domain (T1071.004)
DeviceEvents
| where TimeGenerated > ago(24h)
| where ActionType == "DnsQueryResponse"
| extend DnsQuery = extractjson("$.DnsQueryString", AdditionalFields)
| where DnsQuery endswith ".onion.ws"
or DnsQuery endswith ".tor2web.io"
or DnsQuery matches regex @"^[a-z0-9]{20,}\."
| project TimeGenerated, DeviceName, DnsQuerySentinel Analytics Rule Format
// Rule name: [Descriptive title]
// Description: [What this detects and why]
// MITRE ATT&CK: [Technique IDs]
// Severity: High|Medium|Low|Informational
// Tactics: [InitialAccess, Execution, etc.]
// Query:
[KQL query here]Best Practices
- Always include a time filter (
ago()) to limit query scope - Use
hasinstead ofcontainswhen searching for whole words (faster) - Prefer
in~over multipleorconditions for case-insensitive list matching - Use
letstatements for IOC lists to keep queries readable - Add
projectto select only needed columns (reduces result size) - Test queries on small time windows first before expanding
- Include comments explaining detection logic
Running against a live workspace
This skill authors queries. To run them against a real Microsoft Sentinel workspace — or to hunt interactively — chain /lookup-sentinel. Two rules apply the moment a query targets live data:
- Verify table availability first. The table references in this guide (and in any published hunting query) assume connectors the target environment may not have.
/lookup-sentineldiscovers which tables the workspace actually ingests (tables/ingestion/probe) and adapts — e.g. noDeviceProcessEvents(no Defender for Endpoint) means falling back toSecurityEventEventID 4688, which itself requires command-line auditing. Never ship a hunt referencing unverified tables. - A query that runs is not a query that saw. When a fallback table has weaker fidelity (or the preferred table is absent entirely), state what the environment could not observe alongside the results.
For portable analytics rules meant for any environment, keep the canonical table names from this guide and document the connector prerequisite in the rule header comment.
Output Location
Write KQL queries to: data/detection-rules/kql/<technique-id>-<slug>.kql
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

