{"slug": "irql-md", "title": "IRQL.md", "summary": "Here is a factual summary of the article:\n\nIRQL is a collection of Kusto (KQL) functions designed to unify security logs behind a consistent, analyst-friendly dialect by hiding complexity like schema drift, cluster locations, and join keys behind stable, intention-revealing functions. The functions, created by Saar Ron, John Lambert, and Diana Damenova, fall into five groups (Selectors, Extractors, Enrichers, Graph-lifted variants, and External enrichment) and are designed to compose with graph investigation functions. IRQL aims to reduce cognitive load for both humans and AI by providing a shared vocabulary that makes queries shorter, easier to reason about, and more reliable.", "body_md": "A collection of Kusto (KQL) functions that unify security logs behind a consistent, analyst-friendly dialect. IRQL encapsulates query logic in repeatable chunks, hides cluster/database locations and join keys, and projects disparate source schemas into a single, predictable schema. In addition, it represents query logic as their semantic intent via function naming. These functions were created by Saar Ron, John Lambert, and Diana Damenova.\nThese functions were authored alongside the Lift to Graph functions (Lift_To_Graph\n, Graph_Render_View\n, Graph_Fold_By_Property\n) and are designed to compose with them. Many of the IRQL primitives have a tabular form and a graph-lifted form, so the same logic drives both relational hunts and visual graph investigations.\nKQL is a phenomenal tool for analyzing large quantities of data, but queries can get verbose quickly:\n- Schema drift across tables. The same concept shows up as\nipAddress\n,IPAddress\n,IpAddress\n,ClientIp\n,callerIpAddress\n,cip\n. Timestamps appear asTimestamp\n,TIMESTAMP\n,ReportTime\n,env_time\n,EventTime\n,FirstSeen\n. Analysts waste cycles remembering which spelling applies where. - Cluster and database sprawl. Knowing where each table lives - and which join keys connect them - is tribal knowledge that doesn't scale.\n- Repeated, brittle join logic. The same enrichment patterns get copy-pasted across queries and silently break when upstream schemas shift.\n- Wall-of-text queries. Heavy copy/paste and arcane column parsing make queries long, repetitive, and hard to review.\n- High cognitive load for humans and LLMs alike. Thousands of tables with inconsistent naming is hostile to analyst onboarding and to AI-assisted authoring.\nIRQL addresses this by hiding all of that complexity behind stable, intention-revealing functions:\n- Repeatable selectors encapsulate cluster/database location, join keys, and projection.\n- A unified projected schema (\nEnvTime\n,ClientIp\n,Username\n,Hostname\n,Url\n, …) makes downstream queries look the same regardless of source. - Function names describe intent, not mechanics.\nEnrich_Ip_Employee\n,Extract_Email_Sender_Domain\n,Get_Event_Authentication\n- a pipeline reads as a sequence of meaningful operations rather than a wall of joins, projections, and regexes. Queries are shorter, easier for humans to reason about, and easier for an AI to compose correctly because the names describe what's happening semantically. - A single consistent dialect. Once you learn one selector, every other selector reads the same way.\n- AI-ready by construction. A cohesive domain-specific dialect is dramatically easier for an LLM to generate correctly than raw telemetry, and analyst work written in IRQL becomes higher-quality training signal.\n- Composes with Kusto itself. IRQL is Kusto - every function interoperates cleanly with existing queries, dashboards, and detection pipelines.\nCompounding benefits: faster analyst onboarding, faster query authoring, queries that better reflect their semantic intent, less copy-pasted logic, and a shared vocabulary that both humans and AI can read and write.\nIRQL functions fall into five groups:\n- Selectors -\nGet_*\nfunctions. Return a projected, renamed view of the underlying table using the unified schema. - Extractors -\nExtract_*\nfunctions. Transform fields from existing columns (domain from URL, first name from full name, domain from email sender). - Enrichers -\nEnrich_*\nfunctions. Take a table with a known key column and left-join it against a primitive to add context. - Graph-lifted variants -\nExtract_Node_*\n,Enrich_Node_*\n,Enrich_Graph_*\n. The same primitives, but operating over a graph table produced byLift_To_Graph\nso the results materialize directly into a graph investigation. - External enrichment - two published functions wrapping external threat-intel sources:\nEnrich_Sha256_VirusTotal\nandGet_CISA_KEV\n/Enrich_CISA_KEV\n.\nExamples below use the open KC7 JoJo's Hospital / Valdy Times datasets so you can copy, paste, and run them. Kusto connection string: https://kc7001.eastus.kusto.windows.net.\nThe IRQL pitch is easier to feel than to describe. Here's a real investigation query — surfacing AAD applications that are suddenly seeing sign-ins from a user-agent category they've never used before, a strong signal for token theft, OAuth abuse, and AiTM proxy activity — written first against raw telemetry, then in IRQL.\nThe IRQL primitives shown here don't exist in the published KC7 catalog (which is built around hospital and news-site datasets, not Entra ID sign-ins). They're written in the IRQL idiom to show what the dialect looks like when extended to a real Entra ID estate.\nlet minimumAppThreshold = 100;\nlet timeframe = 1d;\nlet lookback_timeframe = 7d;\nlet ExtractBrowserTypeFromUA = (ua:string) {\ncase(\nua has \"Edge/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Edge\"}),\nua has \"Edg/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Edge\"}),\nua has \"Trident/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Internet Explorer\"}),\nua has \"Chrome/\" and ua has \"Safari/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Chrome\"}),\nua has \"Gecko/\" and ua has \"Firefox/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Firefox\"}),\nnot(ua has \"Mobile/\") and ua has \"Safari/\" and ua has \"Version/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Safari\"}),\nua startswith \"Dalvik/\" and ua has \"Android\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Android Browser\"}),\nua startswith \"MobileSafari//\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Mobile Safari\"}),\nua has \"Mobile/\" and ua has \"Safari/\" and ua has \"Version/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Mobile Safari\"}),\nua has \"Mobile/\" and ua has \"FxiOS/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"IOS Firefox\"}),\nua has \"Mobile/\" and ua has \"CriOS/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"IOS Chrome\"}),\nua has \"Mobile/\" and ua has \"WebKit/\", dynamic({\"AgentType\": \"Browser\", \"AgentName\": \"Mobile Webkit\"}),\nua startswith \"Excel/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Excel\"}),\nua startswith \"Outlook/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Outlook\"}),\nua startswith \"OneDrive/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"OneDrive\"}),\nua startswith \"OneNote/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"OneNote\"}),\nua startswith \"Office/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Office\"}),\nua startswith \"PowerPoint/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"PowerPoint\"}),\nua startswith \"PowerApps/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"PowerApps\"}),\nua startswith \"SharePoint/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"SharePoint\"}),\nua startswith \"Word/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Word\"}),\nua startswith \"Visio/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Visio\"}),\nua startswith \"Whiteboard/\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Whiteboard\"}),\nua =~ \"Mozilla/5.0 (compatible; MSAL 1.0)\", dynamic({\"AgentType\": \"OfficeApp\", \"AgentName\": \"Office Telemetry\"}),\nua has \".NET CLR\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"Dotnet\"}),\nua startswith \"Java/\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"Java\"}),\nua startswith \"okhttp/\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"okhttp\"}),\nua has \"Drupal/\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"Drupal\"}),\nua has \"PHP/\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"PHP\"}),\nua startswith \"curl/\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"curl\"}),\nua has \"python-requests\", dynamic({\"AgentType\": \"Custom\", \"AgentName\": \"Python\"}),\npack(\"AgentType\", \"Other\", \"AgentName\", extract(@\"^([^/]*)/\", 1, ua)))\nlet QueryUserAgents = (start_time:timespan, end_time:timespan) {\nunion withsource=tbl_name AADNonInteractiveUserSignInLogs, SigninLogs\n| where TimeGenerated between (start_time .. end_time)\n| where ResultType == 0\n| extend ParsedUserAgent = ExtractBrowserTypeFromUA(UserAgent)\n| extend UserAgentType = tostring(ParsedUserAgent.AgentType)\n| extend UserAgentName = tostring(ParsedUserAgent.AgentName)\n| extend SimpleUserAgent = UserAgentType\n| where not(isempty(UserAgent))\n| where not(isempty(AppId))};\nlet BaselineUserAgents = materialize(\nQueryUserAgents(lookback_timeframe + timeframe, timeframe)\n| summarize RequestCount = count() by AppId, AppDisplayName, SimpleUserAgent);\nlet BaselineSummarizedAgents = (\nBaselineUserAgents\n| summarize BaselineUAs = make_set(SimpleUserAgent), BaselineRequestCount = sum(RequestCount) by AppId, AppDisplayName);\nQueryUserAgents(timeframe, 0d)\n| summarize count() by AppId, AppDisplayName, UserAgent, SimpleUserAgent\n| join kind=leftanti BaselineUserAgents on AppId, AppDisplayName, SimpleUserAgent\n| join BaselineSummarizedAgents on AppId, AppDisplayName\n| where BaselineRequestCount > minimumAppThreshold\n| join (QueryUserAgents(timeframe, 0d)) on AppId, UserAgent\n| project-away ParsedUserAgent, UserAgentName\n| project-reorder TimeGenerated, AppDisplayName, UserPrincipalName, UserAgent, BaselineUAs\n| summarize count() by UserPrincipalName, AppDisplayName, AppId, UserAgentType, SimpleUserAgent, UserAgent\nThe investigation question — which apps are seeing brand-new UA categories today vs. the past week? — is buried under a 30-branch user-agent classifier inlined as a lambda, a manual union\nacross two sign-in tables, a parameterized subquery invoked at three different time windows, and three explicit joins to stitch baseline against current. The mechanics dominate the intent.\nlet minimumAppThreshold = 100;\nlet timeframe = 1d;\nlet lookback = 7d;\nlet SignInsToday = Get_Event_SignIn(timeframe, 0d);\nlet SignInsBaseline = Get_Event_SignIn(lookback + timeframe, timeframe);\nlet Baseline =\nSignInsBaseline\n| invoke Extract_SignIn_UserAgent_Category()\n| summarize RequestCount = count() by AppId, AppDisplayName, UserAgentCategory;\nSignInsToday\n| invoke Extract_SignIn_UserAgent_Category()\n| invoke Enrich_App_New_UserAgent_Category(Baseline, minimumAppThreshold)\n| project EnvTime, Username, AppDisplayName, AppId, UserAgentCategory, UserAgent, BaselineCategories\n| summarize count() by Username, AppDisplayName, AppId, UserAgentCategory, UserAgent\nSame question, expressed as named operations. Get_Event_SignIn\nis a selector in the Get_Event_*\nfamily — it hides the union\nof AADNonInteractiveUserSignInLogs\nand SigninLogs\n, the success filter, the empty-field guards, and projects into the unified schema. Extract_SignIn_UserAgent_Category\nwraps the 30-branch classifier and adds a single UserAgentCategory\ncolumn, mirroring how Extract_Email_Sender_Domain\nadds a Domain\ncolumn. Enrich_App_New_UserAgent_Category\nencapsulates the baseline-vs-current diff: the leftanti\njoin, the per-app threshold filter, and the BaselineCategories\nset that lets analysts see what was normal alongside what's new.\nThe mechanical work is identical — same union, same classifier, same anti-join, same threshold. The difference is that none of it is in the analyst's face anymore. It lives once, inside the function definitions, and every hunt that touches AAD sign-ins reuses it. The pipeline reads as a sequence of meaningful operations rather than a wall of joins and regex, and an LLM composing a follow-up query has intent-revealing primitives to compose with instead of having to reconstruct the pattern from scratch.\nEach Get_*\nfunction returns a projected view of a source table using the unified schema. Raw security tables typically carry dozens of columns, most of which are irrelevant to any given hunt - agent versions, internal correlation IDs, redundant timestamp variants, schema-versioning fields, and so on. The default Get_*\nform down-projects to just the columns analysts actually reach for day-to-day, renamed into the unified schema. The result is a narrower, more readable table that keeps queries focused and avoids drowning the analyst (or an LLM composing a query) in columns they'll never use. When a hunt legitimately needs the fuller picture, every primitive has a Get_*_All\ncompanion that ", "url": "https://wpnews.pro/news/irql-md", "canonical_source": "https://gist.github.com/ddamenova/a24f3f012012affd017d6bf712f2dd02", "published_at": "2026-04-29 06:39:10+00:00", "updated_at": "2026-05-22 18:09:34.669209+00:00", "lang": "en", "topics": ["cybersecurity", "developer-tools", "data"], "entities": ["Saar Ron", "John Lambert", "Diana Damenova", "IRQL", "Kusto", "KQL", "Lift to Graph"], "alternates": {"html": "https://wpnews.pro/news/irql-md", "markdown": "https://wpnews.pro/news/irql-md.md", "text": "https://wpnews.pro/news/irql-md.txt", "jsonld": "https://wpnews.pro/news/irql-md.jsonld"}}