
위협 헌팅, Azure AD 로그인 로그 분석, 이상 징후 탐지, 그리고 효율적인 탐지 패턴 구축을 위한 Microsoft Sentinel KQL 쿼리와 튜토리얼의 엄선된 컬렉션입니다.
Microsoft Sentinel에서 KQL을 사용하기 위한 몇 가지 팁, 요령 및 예제입니다.
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에 데이터를 조회할 테이블을 알려줍니다. 따라서 이 경우에는 Azure AD 로그인 데이터가 전송되는 SigninLogs 테이블을 검색하려고 합니다. 테이블 목록은 [여기](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
마지막 줄은 project 연산자를 사용하여 로그에서 4개의 필드만 반환하므로, SigninLogs 데이터에서 TimeGenerated, Location, IPAddress 및 UserAgent만 볼 수 있습니다.
이것이 쿼리를 작성하는 방법입니다. 이제 기본 사항입니다.
## 기본 사항
### 시간 기본 사항
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))
이것은 14일 전에서 7일 전 사이의 SigninLogs 데이터를 찾습니다.```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]"
This will search our SigninLogs table, over the last 14 days, for exact matches where our UserPrincipalName equals [email protected]. In KQL == is case sensitive, so if you search for [email protected] and the username is actually [email protected], you won't get any results. The non case sensitive equivalent is =~
이것은 지난 14일 동안 SigninLogs 테이블에서 UserPrincipalName이 [email protected]과 정확히 일치하는 항목을 검색합니다. KQL에서 ==는 대소문자를 구분하므로 [email protected]으로 검색하고 실제 사용자 이름이 [email protected]인 경우 결과를 얻을 수 없습니다. 대소문자를 구분하지 않는 동등 연산자는 =입니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName = "[email protected]"
이것은 대소문자 구분 없이 [email protected]과 일치하는 모든 항목을 찾습니다.
같음 대신에 포함을 사용할 수도 있습니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName contains "reprise_99"
This will find any log entries where the UserPrincipalName contains reprise_99, if you had [email protected] and [email protected] data, it would find both. The contains operator is not case sensitive, but you can use contains_cs to make it case sensitive.
You can use either startswith or endswith if you are searching for particular patterns.```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"
이렇게 하면 애플리케이션 표시 이름에 Teams라는 단어가 포함된 모든 SigninLogs를 찾을 수 있습니다. 여기에는 "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"
이것은 reprise_99를 포함하는 모든 필드에 대해 SigninLogs 테이블을 검색합니다.
이들 옵션 중 상당수는 !를 사용하여 쿼리를 반전시켜 조건이 참이 아닌 결과를 찾는 것도 지원합니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName != "[email protected]"
이 쿼리는 UserPrincipalName이 [email protected]과 같지 않은 모든 SigninLogs를 찾습니다.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName !contains "reprise_99"
이 쿼리는 UserPrincipalName에 reprise_99가 포함되어 있지 않은 모든 SigninLogs를 찾습니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where AppDisplayName !has "Teams"
이 쿼리는 애플리케이션 표시 이름에 "Teams"가 포함되지 않은 SigninLogs를 찾습니다.
Project를 사용하면 쿼리에서 반환되는 열과 그 순서를 선택할 수 있습니다.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | where AppDisplayName == "Microsoft Teams" | project TimeGenerated, Location, IPAddress, UserAgent
이 쿼리는 최근 14일 동안의 SigninLogs 데이터를 검색하며, 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
This returns the same data, but changes the TimeGenerated name to LocalTime and converts to a +5h time zone if you work in that time zone.
project-away is the opposite of project and will remove columns from your query.```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
이 쿼리는 지난 14일 동안의 SigninLogs 테이블에서 [email protected]에 대한 일치 항목을 찾아 결과가 성공(ResultType == 0)인 이벤트를 조회한 다음 해당 이벤트를 애플리케이션 표시 이름별로 요약합니다.
결과 열의 이름을 선택적으로 지정할 수 있습니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize AppCount=count() by AppDisplayName
동일한 데이터를 반환하지만 반환된 열의 이름을 AppCount로 업데이트합니다.
전체 개수 대신 고유 개수(distinct count)를 요약할 수 있습니다.```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")
이는 데이터를 두 개의 새 열, 즉 애플리케이션 표시 이름에 "Teams"가 포함된 TeamsLogons와 애플리케이션 표시 이름에 "SharePoint"가 포함된 SharePointLogons로 요약합니다.
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 bins로 그룹화합니다.
유용한 경우 이러한 함수들을 함께 결합할 수 있습니다.```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 함수의 조합으로, 애플리케이션 표시 이름을 기준으로 요약하고 결과를 1d 빈(bin)에 배치합니다.
쿼리 내에서 항목 집합을 만들 수 있습니다.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize AppList=make_set(AppDisplayName) by UserPrincipalName
이렇게 하면 [email protected]이 로그인한 애플리케이션 목록이 AppList라는 목록으로 출력됩니다.
이것을 우리의 시간 구간과 결합할 수 있습니다.```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
이 쿼리는 지난 14일 동안 [email protected]이 로그인한 모든 애플리케이션을 요약한 다음 출력을 파이차트로 렌더링합니다.

또한 세로 막대형 차트로 렌더링할 수도 있습니다.```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

시간 데이터의 경우, 먼저 summarize 섹션에 설명된 대로 데이터를 시간 'bins'로 요약한 다음, 특정 기간에 걸쳐 데이터를 시각화할 수 있습니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by bin(TimeGenerated, 1d)
| render timechart
이것은 지난 14일 동안 [email protected]의 모든 로그인을 일별로 시각화하여 시간 차트로 표시합니다.

또한 render를 areachart로 사용할 수도 있습니다.```kql SigninLogs | where TimeGenerated > ago(14d) | where UserPrincipalName == "[email protected]" | where ResultType == "0" | summarize SigninCount=count() by bin(TimeGenerated, 1d) | render areachart

세로 막대형 차트와 가로 막대형 차트도 시간 데이터와 함께 사용할 수 있습니다. 더 큰 시간 범위에 걸쳐 시간 'bin'마다 열 또는 막대가 표시됩니다.```kql
SigninLogs
| where TimeGenerated > ago(14d)
| where UserPrincipalName == "[email protected]"
| where ResultType == "0"
| summarize SigninCount=count() by bin(TimeGenerated, 1d)
| render columnchart
이는 타임차트와 동일한 쿼리이지만, 일별로 하나의 열을 가지는 세로 막대형 차트(column chart)로 렌더링한 것입니다.
```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
이 쿼리는 우리 계정의 모든 로그인을 찾아 애플리케이션별 로그인 수를 집계한 다음, 각 날짜에 대해 단일 열을 생성합니다.

각 애플리케이션이 자체 열을 가지도록 하려면 스택 해제(unstacked)로 설정할 수 있습니다.```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은 구분자를 기준으로 텍스트 문자열을 배열로 분리합니다. 원래 테스트 데이터로 돌아가면, 쉼표 기호를 기준으로 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"
"Reprise99"가 Name에 포함된 경우 하나의 적중 결과만 얻을 수 있고, "Reprise103"이 Name에 포함된 두 번째 레코드는 찾을 수 없습니다.
