
Курируемая коллекция KQL-запросов и руководств по Microsoft Sentinel для охоты на угрозы, анализа журналов входа Azure AD, обнаружения аномалий и создания эффективных шаблонов обнаружения.
Некоторые советы, приёмы и примеры использования KQL для Microsoft Sentinel.
Kusto Query Language — это язык, используемый в Azure Monitor, Azure Data Explorer и Azure Log Analytics (то, что Microsoft Sentinel использует под капотом). Мне всегда казалась полезной эта визуализация, касающаяся KQL, —

Мы хотим использовать KQL для создания точных и эффективных запросов, чтобы находить угрозы, детектируемые события, закономерности и аномалии в нашем большом наборе данных.
Возьмём приведённый ниже запрос в качестве примера```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | where AppDisplayName == "Microsoft Teams" | project TimeGenerated, Location, IPAddress, UserAgent
Когда мы выполняем такой запрос, первая строка указывает Microsoft Sentinel, в какой таблице искать данные, поэтому в данном случае мы хотим искать в таблице SigninLogs, куда отправляются данные о входе в Azure AD. Список таблиц можно посмотреть [здесь](https://docs.microsoft.com/en-us/azure/sentinel/data-source-schema-reference).
Затем Microsoft Sentinel будет последовательно выполнять ваш запрос, то есть обрабатывать каждую строку одну за другой, пока не дойдёт до конца или не возникнет ошибка. Итак, разберём наш запрос построчно.```kql
SigninLogs
Итак, сначала мы выбрали нашу таблицу SigninLogs.```kql SigninLogs | where TimeGenerated > ago(14d)
Затем мы указываем Sentinel просмотреть данные за последние 14 дней в этой таблице.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
Далее мы просим Sentinel находить только логи, где UserPrincipalName равен "[email protected]"```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0"
Затем мы ищем только те журналы, в которых ResultType == 0, что соответствует успешному входу в Azure AD.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| where AppDisplayName == "Microsoft Teams"
Далее мы ищем только входы в Microsoft Teams.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | where AppDisplayName == "Microsoft Teams" | project TimeGenerated, Location, IPAddress, UserAgent
Наша последняя строка использует оператор проекта, чтобы вернуть только 4 поля из наших журналов, поэтому мы увидим только TimeGenerated, Location, IPAddress и UserAgent, возвращённые из данных SigninLogs.
Вот так вы строите запросы, теперь основы.
## Основы
### Основы работы со временем
Microsoft Sentinel и KQL сильно оптимизированы для фильтров по времени, поэтому если вы знаете период данных, которые хотите найти, следует сразу же отфильтровать временной диапазон. Например, получить журналы за последние 14 дней, а затем искать имя пользователя, как в запросе ниже —```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
Гораздо эффективнее, чем сначала искать имя пользователя, а затем искать временной период, вот так —```kql SigninLogs | where UserPrincipalName == "[email protected]" | where TimeGenerated > ago(14d)
KQL предоставляет множество вариантов для запроса конкретных временных периодов.```kql
SigninLogs
| where TimeGenerated > ago(14d)
Как и в первом примере, это будет выполнять поиск за последние 14 дней.```kql SigninLogs | where TimeGenerated > ago(14h)
Вы также можете указать часы.```kql
SigninLogs
| where TimeGenerated > ago(14m)
И минуты.
KQL также поддерживает запросы между диапазонами времени -```kql SigninLogs | where TimeGenerated between (ago(14d) .. ago(7d))
Это позволит найти данные SigninLogs за период от 14 до 7 дней назад.```kql
SigninLogs
| where TimeGenerated between (ago(14h) .. ago(7h))
Между 14 и 7 часами назад.```kql SigninLogs | where TimeGenerated between (ago(14m) .. ago(7m))
И в промежутке от 14 минут до 7 минут назад.
### Основы where
Where — это оператор, который вы будете использовать практически в каждом запросе. Так вы указываете Microsoft Sentinel, какие конкретные данные искать. Синтаксис очень важен для оператора where. Если мы используем тот же пример.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
Это позволит выполнить поиск по нашей таблице SigninLogs за последние 14 дней на предмет точных совпадений, где наш UserPrincipalName равен [email protected]. В KQL оператор == чувствителен к регистру, поэтому если вы ищете [email protected], а имя пользователя на самом деле [email protected], вы не получите никаких результатов. Нечувствительный к регистру эквивалент — это =```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName = "[email protected]"
Это найдет все совпадения для [email protected] независимо от регистра.
Вместо equals можно также использовать contains.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName contains "reprise_99"
Это найдет все записи журнала, где UserPrincipalName содержит reprise_99: если у вас есть данные [email protected] и [email protected], будут найдены обе. Оператор contains не чувствителен к регистру, но вы можете использовать contains_cs, чтобы сделать его чувствительным к регистру.
Вы можете использовать либо startswith, либо endswith при поиске определенных шаблонов.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName startswith "reprise_99"
SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName endswith "testdomain.com"
Операторы startswith и endswith не чувствительны к регистру, но вы можете использовать startswith_cs или endswith_cs, чтобы сделать их чувствительными к регистру.
Если вы ищете полные слова (длиннее четырёх символов), в KQL вы можете использовать оператор has. Использование 'has' более эффективно, чем 'contains', поскольку данные индексируются для вас.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where AppDisplayName has "Teams"
Это позволит найти любые журналы SigninLogs, где в отображаемом имени приложения есть слово Teams; это может включать "Microsoft Teams" и "Microsoft Teams Web Client" — оба удовлетворяют запросу.
Если вы ищете несколько слов, можно использовать has_any или has_all.```kql SigninLogs | where TimeGenerated > ago(14d) | where AppDisplayName has_any ("Teams","Outlook")
Это вернёт результаты, где отображаемое имя приложения содержит либо "Teams", либо "Outlook"```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where AppDisplayName has_all ("Teams","Outlook")
Это вернёт результаты, в которых отображаемое имя приложения содержит "Teams" и "Outlook".
Если вы не знаете, по каким полям искать, можно также использовать подстановочные знаки — это неэффективно, но может навести вас на правильный путь, чтобы найти то, что вам нужно.```kql SigninLogs | where TimeGenerated > ago(14d) | where * contains "reprise_99"
Это позволит выполнить поиск в таблице SigninLogs по любому полю, содержащему reprise_99.
Ряд этих опций также поддерживает использование ! для инвертирования запроса и поиска результатов, где условие не выполняется.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName != "[email protected]"
Этот запрос найдёт все SigninLogs, где UserPrincipalName не равен [email protected]```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName !contains "reprise_99"
Этот запрос найдёт все SigninLogs, где UserPrincipalName не содержит reprise_99```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where AppDisplayName !has "Teams"
Этот запрос найдет SigninLogs, где отображаемое имя приложения не содержит "Teams".
Project позволяет нам выбирать, какие столбцы возвращаются в нашем запросе и в каком порядке.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | where AppDisplayName == "Microsoft Teams" | project TimeGenerated, Location, IPAddress, UserAgent
Этот запрос ищет данные SigninLogs за последние 14 дней, где UserPrincipalname равен [email protected], где ResultType равен 0, где отображаемое имя приложения равно "Microsoft Teams"; затем для каждого совпадения по этому запросу возвращаются TimeGenerated, Location, IPAddress и UserAgent.
Мы можем переименовать столбцы в рамках той же функции.```kql
| project LogTime=TimeGenerated, SigninLocation=Location, IP=IPAddress, Agent=UserAgent
Это возвращает те же данные, но переименовывает столбцы в LogTime, SigninLocation, IP и Agent.
Мы даже можем манипулировать выходными данными непосредственно с помощью оператора project.```kql | project LocalTime=TimeGenerated+5h, Location, IPAddress, UserAgent
Это возвращает те же данные, но изменяет имя TimeGenerated на LocalTime и преобразует в часовой пояс +5h, если вы работаете в этом часовом поясе.
project-away противоположен project и удаляет столбцы из вашего запроса.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| project-away UserAgent
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| where AppDisplayName == "Microsoft Teams"
В этом запросе мы удаляем UserAgent. Помните: если вы удаляете столбец, вы не сможете получить к нему доступ позже в запросе.
Summarize создаёт таблицу, агрегирующую содержимое вашего запроса. Summarize имеет ряд базовых агрегатных функций. Если снова взять наш пример запроса, мы можем по-разному манипулировать результатами с помощью summarize.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize count() by AppDisplayName
Этот запрос ищет в таблице SigninLogs любые события за последние 14 дней, соответствующие [email protected], где результат является успешным (ResultType == 0), а затем сводит эти события по отображаемому имени приложения.
Вы можете при желании задать имя столбца результата.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize AppCount=count() by AppDisplayName
Это возвращает те же данные, но переименовывает возвращаемый столбец в AppCount.
Вместо общего количества можно выполнить сводный подсчёт уникальных значений.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize DistinctAppCount=dcount(AppDisplayName) by AppDisplayName
Это вернёт одну запись для каждого отдельного приложения, в которое выполнил вход пользователь [email protected].
Вы можете использовать функции arg_max и arg_min, чтобы вернуть самую новую или самую старую запись, соответствующую вашему запросу.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize arg_max(TimeGenerated, *) by UserPrincipalName
Этот запрос ищет все журналы входа за последние 14 дней, у которых UserPrincipalname — [email protected], которые успешны, и затем возвращает последнюю запись.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize arg_min(TimeGenerated, *) by UserPrincipalName
Это то же самое, но возвращает самую старую запись.
Вы можете использовать countif, чтобы задать логику для ваших суммирований.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize TeamsLogons=countif(AppDisplayName has "Teams"), SharePointLogons=countif(AppDisplayName has "SharePoint")
Это обобщает данные в два новых столбца: TeamsLogons, где отображаемое имя приложения содержит "Teams", и SharePointLogons, где отображаемое имя приложения содержит "SharePoint"
Вы можете дополнительно манипулировать своими данными, указав KQL поместить ваши данные во временные 'bins'.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize AppCount=count() by AppDisplayName, bin(TimeGenerated, 1d)
Это возвращает те же данные, что и наш первый пример summarize, а затем группирует эти данные в 1d-интервалы.
Вы можете комбинировать эти функции друг с другом там, где это полезно.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize TeamsLogons=countif(AppDisplayName has "Teams"), SharePointLogons=countif(AppDisplayName has "SharePoint") by bin(TimeGenerated, 1d)
Это комбинация наших функций countif и bin, где мы выполняем сводку на основе отображаемого имени приложения, а также помещаем результаты в одномерные бины.
Вы можете создать набор элементов в рамках запроса.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize AppList=make_set(AppDisplayName) by UserPrincipalName
Это выведет список приложений, в которые выполнял вход [email protected], в список с названием AppList.
Вы можете объединить это с нашим time bin.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize AppList=make_set(AppDisplayName) by UserPrincipalName, bin(TimeGenerated, 1d)
Это создаст список приложений, в которые входил пользователь [email protected], с разбивкой на один список в день.
Оператор render позволяет KQL визуализировать данные в различных форматах, таких как круговые диаграммы, временные или площадные диаграммы, а также столбчатые и линейчатые диаграммы.
Если мы воспользуемся тем же примером из нашей таблицы Signinlogs, мы увидим, как можно визуализировать данные различными способами.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize AppCount=count()by AppDisplayName | render piechart
Этот запрос обобщает все приложения, в которые [email protected] выполнял вход за последние 14 дней, а затем отображает результат в виде круговой диаграммы.

Вы также можете отобразить результат в виде столбчатой диаграммы.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize AppCount=count()by AppDisplayName
| render columnchart

Или столбчатая диаграмма.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize AppCount=count()by AppDisplayName | render barchart

Для временных данных сначала обобщите данные во временные «bins», как описано в разделе summarize, а затем визуализируйте свои данные за определенный период времени.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by bin(TimeGenerated, 1d)
| render timechart
Это визуализирует все входы в систему пользователя [email protected] по дням за последние 14 дней и отображает их в виде временной диаграммы (timechart).

Вы также можете использовать render для построения диаграммы с областями (areachart).```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize SigninCount=count() by bin(TimeGenerated, 1d) | render areachart

Столбчатые и линейчатые диаграммы также можно использовать с временными данными. Вы получите столбец или полосу на каждый временной «бин» в пределах вашего более крупного временного периода.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by bin(TimeGenerated, 1d)
| render columnchart
Это тот же запрос, что и наш timechart, но отображается в виде столбчатой диаграммы с одним столбцом в день.
```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by bin(TimeGenerated, 1d)
| render barchart
И столбчатая диаграмма.

Столбчатые и линейчатые диаграммы можно складывать друг на друга (это режим по умолчанию).```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by AppDisplayName, bin(TimeGenerated, 1d)
| render columnchart
Этот запрос находит все входы в нашу учетную запись, подсчитывает входы по каждому приложению, а затем создает отдельный столбец для каждого дня.

Если вы хотите, чтобы у каждого приложения был свой столбец, вы можете отключить накопление.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize SigninCount=count() by bin(TimeGenerated, 1d) | render columnchart with (kind=unstacked)

Вы также можете переименовать ось и заголовок диаграммы с помощью KQL.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by AppDisplayName, bin(TimeGenerated, 1d)
| render columnchart with (kind=unstacked, ytitle="Total Sign Ins", xtitle="Day", title="Application Signins Per Day")

Вы можете комбинировать логику в действиях summarize, чтобы создавать динамическое содержимое для оператора render.```kql SigninLogs | where TimeGenerated > ago(14d) | where ResultType == "0" | summarize TeamsCount=countif(AppDisplayName has "Teams"), OneDrive=countif(AppDisplayName has "OneDrive"), SharePointCount=countif(AppDisplayName has "SharePoint") by bin(TimeGenerated, 1d) | render columnchart with (kind=unstacked, ytitle="Sign In Count", xtitle="Day", title="Teams vs OneDrive vs SharePoint Sign Ins Per Day")
Этот запрос ищет все входы в ваш клиент, затем подсчитывает три группы: одну, где отображаемое имя приложения содержит "Teams", одну, где отображаемое имя приложения содержит "OneDrive", и одну, где отображаемое имя приложения содержит "SharePoint", для каждого дня за последние 14 дней, а затем выводит результат в виде столбчатой диаграммы без накопления.

### Основы Parse и Split
Parse и split — это два разных способа разбить строку данных на несколько столбцов на основе совпадений. Многие журналы, поступающие в Microsoft Sentinel, могут выглядеть как одна длинная строка (например, sysmon); parse и split позволяют преобразовать их в читаемые данные.
Для этих примеров мы будем использовать следующие тестовые данные```kql
let ExampleText = datatable(TestData:string)
[
'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account'
];
Эти данные — просто одна строка, которая выглядит так.

Мы можем извлечь определённые совпадения данных с помощью следующего```kql let ExampleText = datatable(TestData:string) [ 'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account' ]; ExampleText | parse TestData with * 'Name=' DisplayName ',' * | project DisplayName
Это извлечёт все данные между `Name=` и `,` в новую колонку с именем `DisplayName`.

Вы можете извлечь несколько колонок в одной команде, сопоставляя по строке.```kql
let ExampleText = datatable(TestData:string)
[
'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account'
];
ExampleText
| parse TestData with * 'Name=' DisplayName ',UPNSuffix=' DomainSuffix ',AadTenantId=' AzureADTenantId ',' *
| project DisplayName, DomainSuffix, AzureADTenantId
Это разбирает три новых столбца - DisplayName, DomainSuffix и AzureADTenantId

Помня, что KQL выполняет свои операции последовательно, после разбора мы можем снова выполнить разбор уже по вновь созданному столбцу.```kql let ExampleText = datatable(TestData:string) [ 'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account', ]; ExampleText | parse TestData with * 'Name=' DisplayName ',UPNSuffix=' DomainSuffix ',AadTenantId=' AzureADTenantId ',' * | project DisplayName, DomainSuffix, AzureADTenantId | parse DomainSuffix with * '.' TopLevelDomain | project DisplayName, DomainSuffix, TopLevelDomain, AzureADTenantId
Это дополнительно разбирает наш домен, чтобы найти домен верхнего уровня, в данном случае .com

При использовании оператора parse KQL проходит по всем вашим строкам данных и возвращает даже результаты, где нет совпадения. Поэтому в зависимости от структуры ваших данных вы можете получить множество строк с пустыми данными. Если мы расширим наши примерные данные, добавив ещё одну строку с другими именами, и выполним тот же запрос, вы увидите пустые результаты.```kql
let ExampleText = datatable(TestData:string)
[
'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account',
'Display=Reprise99,UPN=testdomain.com,AadDirectoryId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadObjectId=cf6f2df6-b754-48dc-b7bc-c8339caf211,Name=Test User,AccountType=account'
]
;
ExampleText
| parse TestData with * 'Name=' DisplayName ',UPNSuffix=' DomainSuffix ',AadTenantId=' AzureADTenantId ',' *
| project DisplayName, DomainSuffix, AzureADTenantId
| parse DomainSuffix with * '.' TopLevelDomain
| project DisplayName, DomainSuffix, TopLevelDomain, AzureADTenantId

Чтобы бороться с этим, вы можете использовать оператор 'parse-where', который возвращает только те результаты, в которых есть совпадения с вашим запросом.```kql let ExampleText = datatable(TestData:string) [ 'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account', 'Display=Reprise99,UPN=testdomain.com,AadDirectoryId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadObjectId=cf6f2df6-b754-48dc-b7bc-c8339caf211,Name=Test User,AccountType=account' ] ; ExampleText | parse-where TestData with * 'Name=' DisplayName ',UPNSuffix=' DomainSuffix ',AadTenantId=' AzureADTenantId ',' * | project DisplayName, DomainSuffix, AzureADTenantId | parse DomainSuffix with * '.' TopLevelDomain | project DisplayName, DomainSuffix, TopLevelDomain, AzureADTenantId
Мы видим, что снова получили единственный результат, в котором у нас было совпадение при парсинге.

Split разделяет строку текста на массив на основе разделителя. Если мы вернёмся к нашим исходным тестовым данным, мы можем разбить их по запятой.```kql
let ExampleText = datatable(TestData:string)
[
'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account',
]
;
ExampleText
| extend SplitData = split(TestData,',')
| project SplitData
Мы получим массив с разделенной строкой.

Split учитывает индексы, поэтому если ваши данные расположены в том же порядке, вы можете разбивать их непосредственно на новые столбцы.```kql let ExampleText = datatable(TestData:string) [ 'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account', ] ; ExampleText | extend Name = split(TestData,',')[0] | extend DomainSuffix = split(TestData,',')[1] | extend AzureADTenantId = split(TestData,',')[2] | extend AzureADUserId = split(TestData,',')[3] | extend DisplayName = split(TestData,',')[4] | extend AccountType = split(TestData,',')[5] | project Name, DomainSuffix, AzureADTenantId, AzureADUserId, DisplayName, AccountType
Если нам известно расположение данных в строке, мы можем разбить её напрямую на именованные столбцы.

После того как мы разбили наши данные, мы можем выполнять по ним запросы так, как будто они были структурированы с самого начала. Так что если мы добавим вторую запись к нашим данным, а затем выполним поиск по конкретным совпадениям, мы найдём то, что ищем.```kql
let ExampleText = datatable(TestData:string)
[
'Name=Reprise99,UPNSuffix=testdomain.com,AadTenantId=345c1234-a833-43e4-1d34-123440a5bcdd1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User,Type=account',
'Name=Reprise103,UPNSuffix=testdomain.com,AadTenantId=331c1234-a841-43e5-1d31-12220a5bcee1,AadUserId=cf6f2df6-b754-48dc-b7bc-c8339caf211,DisplayName=Test User 2,Type=account'
]
;
ExampleText
| extend Name = split(TestData,',')[0]
| extend DomainSuffix = split(TestData,',')[1]
| extend AzureADTenantId = split(TestData,',')[2]
| extend AzureADUserId = split(TestData,',')[3]
| extend DisplayName = split(TestData,',')[4]
| extend AccountType = split(TestData,',')[5]
| project Name, DomainSuffix, AzureADTenantId, AzureADUserId, DisplayName, AccountType
| where Name contains "Reprise99"
Мы получаем только одно совпадение, где Name содержит «Reprise99»; наша вторая запись, где Name содержит «Reprise103», не найдена.
