Microsoft Sentinel의 Kusto 쿼리 언어

Kusto 쿼리 언어는 Microsoft Sentinel에서 데이터로 작업하고 조작하는 데 사용하는 언어입니다. 작업 영역에 제공하는 로그를 분석하고 모든 데이터에 숨겨진 중요한 정보를 얻을 수 없다면 로그는 별로 가치가 없습니다. Kusto 쿼리 언어는 해당 정보를 얻을 수 있는 능력과 유연성이 있을 뿐만 아니라 간단하기 때문에 빠르게 시작하는 데 도움이 됩니다. 스크립팅 또는 데이터베이스 작업 경험이 있는 경우 이 문서의 많은 내용이 매우 익숙하게 느껴질 것입니다. 그렇지 않더라도 언어의 직관적인 특성상, 신속하게 고유한 쿼리를 작성하고 조직을 위한 가치를 창출할 수 있으므로 걱정할 필요가 없습니다.

이 문서에서는 Kusto 쿼리 언어의 기본 사항을 소개하고 매일 작성하는 쿼리의 75~80%를 처리할 것으로 예상되는, 가장 많이 사용되는 몇 가지 함수와 연산자에 대해 설명합니다. 심층 지식이 필요하거나 고급 쿼리를 실행하려는 경우 새로운 Microsoft Sentinel용 고급 KQL 통합 문서를 활용할 수 있습니다(소개 블로그 게시물 참조). 공식 Kusto 쿼리 언어 설명서와 다양한 온라인 과정(예: Pluralsight)도 참조하세요.

배경 - Kusto 쿼리 언어를 사용하는 이유

Microsoft Sentinel은 Azure Monitor 서비스를 토대로 빌드되었으며, Azure Monitor의 Log Analytics 작업 영역을 사용하여 모든 데이터를 저장합니다. 이 데이터에는 다음이 포함됩니다.

  • Microsoft Sentinel 데이터 커넥터를 사용하여 외부 원본에서 미리 정의된 테이블로 수집된 데이터
  • 사용자 지정해서 만든 데이터 커넥터 및 일부 유형의 기본 제공 커넥터를 사용하여 외부 원본에서 사용자 정의된 사용자 지정 테이블로 수집된 데이터
  • Microsoft Sentinel이 만들고 수행하는 분석에서 생성된, Microsoft Sentinel 자체에서 만든 데이터(예: 경고, 인시던트, UEBA 관련 정보)
  • 검색 및 분석을 지원하기 위해 Microsoft Sentinel에 업로드된 데이터(예: 위협 인텔리전스 피드, 관심 목록)

Kusto 쿼리 언어Azure Data Explorer 서비스의 일부로 개발되었으므로 클라우드 환경의 빅 데이터 저장소 검색에 최적화되어 있습니다. 유명한 해저 탐험가 Jacques Cousteau에서 영감을 받아(그에 따라 “koo-STOH”로 발음됨) 데이터 바다 속 깊이 잠수하여 숨겨진 보물을 탐사할 수 있도록 설계되었습니다.

또한 Kusto 쿼리 언어는 몇 가지 추가 Azure Monitor 기능을 포함하여 Azure Monitor(및 Microsoft Sentinel)에서 Log Analytics 데이터 저장소의 데이터를 검색, 시각화, 분석, 구문 분석할 수 있습니다. Microsoft Sentinel에서 기존 규칙 및 통합 문서를 사용하든 또는 고유한 규칙 및 통합 문서를 빌드하든 관계없이 데이터를 시각화 및 분석하고 위협을 헌팅할 때마다 Kusto 쿼리 언어를 기반으로 하는 도구를 사용합니다.

Kusto 쿼리 언어는 Microsoft Sentinel에서 수행하는 거의 모든 작업에 포함되므로 작동 방식을 명확하게 이해하면 SIEM을 훨씬 효율적으로 활용하는 데 도움이 됩니다.

쿼리란?

Kusto 쿼리 언어 쿼리는 데이터를 처리하고 결과를 반환하라는 읽기 전용 요청으로, 데이터를 쓰지 않습니다. 쿼리는 SQL과 유사하게 데이터베이스, 테이블, 의 계층 구조로 이루어진 데이터에서 작동합니다.

요청은 일반 언어로 작성되며, 쉽게 읽고, 쓰고, 자동화할 수 있는 구문을 만들도록 설계된 데이터 흐름 모델을 사용합니다. 자세히 살펴보겠습니다.

Kusto 쿼리 언어 쿼리는 세미콜론으로 구분된 문으로 구성됩니다. 다양한 종류의 문이 있지만, 여기서는 널리 사용되는 두 가지 유형만 설명합니다.

  • 테이블 형식 식 문은 쿼리에 대해 말할 때 일반적으로 의미하는 것으로, 쿼리의 실제 본문입니다. 테이블 형식 식 문에 대해 알아야 할 중요한 점은 테이블 형식 입력(테이블 또는 다른 테이블 형식 식)을 수락하고 테이블 형식 출력을 생성한다는 것입니다. 최소한 한 개는 필수입니다. 이 문서의 나머지 부분에서는 대체로 이 종류의 문에 대해 설명합니다.

  • let을 사용하면 읽기 쉽고 융통성이 있도록 쿼리 본문 외부에서 변수와 상수를 만들고 정의할 수 있습니다. 이 문은 선택 사항이며 특정 요구 사항에 따라 다릅니다. 문서의 끝 부분에서 이 종류의 문에 대해 설명하겠습니다.

데모 환경

Azure Portal의 Log Analytics 데모 환경에서 이 문서에 있는 문을 비롯한 Kusto 쿼리 언어 문을 연습할 수 있습니다. 이 연습 환경에 대한 사용 요금은 없지만 연습 환경에 액세스하려면 Azure 계정이 필요합니다.

데모 환경을 살펴봅니다. 프로덕션 환경의 Log Analytics와 마찬가지로, 다음과 같은 다양한 방식으로 사용할 수 있습니다.

  • 쿼리를 빌드할 테이블을 선택합니다. 기본 테이블 탭(왼쪽 위에 있는 빨간색 사각형에 표시됨)에서 토픽별로 그룹화된 테이블 목록(왼쪽 아래에 표시됨)을 통해 테이블을 선택합니다. 토픽을 펼쳐 개별 테이블을 확인합니다. 각 테이블을 추가로 펼쳐 모든 필드(열)를 확인할 수 있습니다. 테이블 또는 필드 이름을 두 번 클릭하면 쿼리 창의 커서 지점에 배치됩니다. 아래와 같이 테이블 이름 뒤에 쿼리의 나머지 부분을 입력합니다.

  • 연구하거나 수정할 기존 쿼리를 찾습니다. 쿼리 탭(왼쪽 위에 있는 빨간색 사각형에 표시됨)을 선택하여 기본 제공 쿼리 목록을 확인합니다. 또는 오른쪽 위에 있는 단추 모음에서 쿼리를 선택합니다. Microsoft Sentinel과 함께 기본 제공되는 쿼리를 살펴볼 수 있습니다. 쿼리를 두 번 클릭하면 쿼리 창의 커서 지점에 전체 쿼리가 배치됩니다.

    Shows the Log Analytics demo environment.

이 데모 환경과 마찬가지로, Microsoft Sentinel 로그 페이지에서 데이터를 쿼리하고 필터링할 수 있습니다. 테이블을 선택하고 드릴다운하여 열을 확인할 수 있습니다. 열 선택을 사용하여 표시되는 기본 열을 수정할 수 있으며, 쿼리의 기본 시간 범위를 설정할 수 있습니다. 쿼리에서 시간 범위가 명시적으로 정의된 경우에는 시간 필터를 사용할 수 없습니다(회색으로 표시됨).

쿼리 구조

Kusto 쿼리 언어 학습을 시작하기에 좋은 지점은 전체 쿼리 구조를 파악하는 것입니다. Kusto 쿼리를 볼 때 가장 먼저 눈에 띄는 것은 파이프 기호(|)를 사용하는 것입니다. Kusto 쿼리의 구조는 데이터 원본에서 데이터를 가져온 후 “파이프라인”을 통해 데이터를 전달하는 것으로 시작하며, 각 단계에서 일정 수준의 처리를 제공하고 데이터를 그 다음 단계로 전달합니다. 파이프라인이 끝나면 최종 결과를 얻게 됩니다. 실제로 예제 파이프라인은 다음과 같습니다.

Get Data | Filter | Summarize | Sort | Select

파이프라인 아래로 데이터를 전달하는 이 개념은 각 단계에서 데이터의 심상 그림을 쉽게 만들 수 있으므로 매우 직관적인 구조에 도움이 됩니다.

이를 설명하기 위해 Microsoft Entra 로그인 로그를 살펴보는 다음 쿼리를 살펴보겠습니다. 각 줄을 읽을 때 데이터에 수행되는 작업을 나타내는 키워드를 확인할 수 있습니다. 파이프라인의 관련 단계를 각 줄에 주석으로 포함했습니다.

참고 항목

앞에 이중 슬래시(//)를 추가하여 쿼리의 모든 줄에 주석을 추가할 수 있습니다.

SigninLogs                              // Get data
| evaluate bag_unpack(LocationDetails)  // Ignore this line for now; we'll come back to it at the end.
| where RiskLevelDuringSignIn == 'none' // Filter
   and TimeGenerated >= ago(7d)         // Filter
| summarize Count = count() by city     // Summarize
| sort by Count desc                    // Sort
| take 5                                // Select

모든 단계의 출력이 다음 단계의 입력으로 사용되므로 단계 순서가 쿼리 결과를 결정하고 성능에 영향을 줄 수 있습니다. 쿼리에서 얻으려는 결과에 따라 단계 순서를 지정하는 것이 중요합니다.

  • 경험상, 데이터를 초기에 필터링하여 관련 데이터만 파이프라인 아래로 전달하는 것이 좋습니다. 이렇게 하면 성능이 훨씬 향상되고, 요약 단계에서 관련 없는 데이터가 실수로 포함되는 것을 방지할 수 있습니다.
  • 이 문서에서는 유의해야 할 몇 가지 다른 모범 사례를 제시합니다. 자세한 목록은 쿼리 모범 사례를 참조하세요.

지금까지 Kusto 쿼리 언어로 작성된 쿼리의 전체 구조를 알아보았습니다. 이제 쿼리를 만드는 데 사용되는 실제 쿼리 연산자 자체를 살펴봅시다.

데이터 형식

쿼리 연산자에 앞서 데이터 형식을 먼저 간단히 살펴봅시다. 대부분의 언어와 마찬가지로, 데이터 형식은 값에 대해 실행할 수 있는 계산과 조작을 결정합니다. 예를 들어 문자열 형식의 값이 있는 경우 해당 값에 대해 산술 계산을 수행할 수 없습니다.

Kusto 쿼리 언어에서 대부분의 데이터 형식은 표준 규칙을 따르며, 이전에 본 적이 있는 이름을 사용합니다. 다음 표에는 전체 목록이 나와 있습니다.

데이터 형식 표

Type 추가 이름 해당하는.NET 형식
bool Boolean System.Boolean
datetime Date System.DateTime
dynamic System.Object
guid uuid, uniqueid System.Guid
int System.Int32
long System.Int64
real Double System.Double
string System.String
timespan Time System.TimeSpan
decimal System.Data.SqlTypes.SqlDecimal

대부분의 데이터 형식이 표준이지만 동적, 시간 범위, guid와 같은 형식은 덜 익숙할 수 있습니다.

동적 구조는 JSON과 매우 유사하지만 한 가지 주요 차이점이 있습니다. 중첩된 동적 값이나 시간 범위와 같이 기존 JSON에서 사용할 수 없는 Kusto 쿼리 언어별 데이터 형식을 저장할 수 있습니다. 동적 형식의 예제는 다음과 같습니다.

{
"countryOrRegion":"US",
"geoCoordinates": {
"longitude":-122.12094116210936,
"latitude":47.68050003051758
},
"state":"Washington",
"city":"Redmond"
}

시간 범위는 시간, 일 또는 초와 같은 시간 측정값을 가리키는 데이터 형식입니다. 시간 측정값이 아닌 실제 날짜 및 시간으로 평가되는 날짜/시간과 시간 범위를 혼동하지 마세요. 다음 표에는 시간 범위 접미사 목록이 나와 있습니다.

시간 범위 접미사

함수 설명
D
H 시간
M minutes
S
Ms 밀리초
Microsecond 마이크로초
Tick 나노초

Guid는 [8]-[4]-[4]-[4]-[12]의 표준 형식을 따르는 128비트 GUID를 나타내는 데이터 형식입니다. 여기서 각 [숫자]는 문자 수를 나타내고, 각 문자의 범위는 0~9 또는 a~f일 수 있습니다.

참고 항목

Kusto 쿼리 언어에는 테이블 형식 연산자와 스칼라 연산자가 둘 다 있습니다. 이 문서의 나머지 부분에서 단순히 “연산자”라는 단어를 발견할 경우, 달리 명시되지 않는 한 테이블 형식 연산자를 의미한다고 가정할 수 있습니다.

데이터 가져오기, 제한, 정렬, 필터링

대부분의 작업을 수행할 수 있는 기반인 Kusto 쿼리 언어의 핵심 용어 모음은 데이터를 필터링, 정렬, 선택하기 위한 연산자 컬렉션입니다. 나머지 작업을 수행하려면 고급 요구 사항에 맞게 언어에 대한 지식을 늘려야 합니다. 위 예제에서 사용한 몇 가지 명령을 약간 확장하여 take, sort, where를 살펴봅시다.

이전 SigninLogs 예제에서 각 연산자의 사용을 살펴보고 유용한 팁 또는 모범 사례를 알아보겠습니다.

데이터 가져오기

기본 쿼리의 첫 번째 줄은 작업할 테이블을 지정합니다. Microsoft Sentinel의 경우 일반적으로 SigninLogs, SecurityAlert 또는 CommonSecurityLog와 같은 작업 영역의 로그 유형 이름입니다. 예시:

SigninLogs

Kusto 쿼리 언어에서 로그 이름은 대/소문자를 구분하므로 SigninLogssigninLogs가 다르게 해석됩니다. 사용자 지정 로그의 이름을 선택할 때는 쉽게 식별할 수 있고 다른 로그와 너무 유사하지 않도록 주의해야 합니다.

데이터 제한: take / limit

take 연산자(및 동일한 limit 연산자)는 지정된 개수의 행만 반환하여 결과를 제한하는 데 사용됩니다. 연산자 뒤에는 반환할 행 수를 지정하는 정수가 옵니다. 일반적으로 정렬 순서를 결정한 후 쿼리의 끝 부분에서 사용되며, 이 경우 정렬된 순서의 맨 위에서 지정된 개수의 행을 반환합니다.

쿼리의 앞부분에서 take를 사용하면 대규모 데이터 세트를 반환하지 않으려는 경우 쿼리를 테스트하는 데 유용할 수 있습니다. 그러나 take 연산을 sort 연산 앞에 배치할 경우 take는 임의로 선택된 행을 반환하며, 쿼리를 실행할 때마다 다른 행 집합이 반환될 수 있습니다. take 사용 예제는 다음과 같습니다.

SigninLogs
      | take 5

Screenshot of results of take operator.

쿼리가 어떻게 표시될지 알 수 없는 새로운 쿼리를 작업하는 경우 더 빠른 처리 및 실험을 위해 take 문을 시작 부분에 배치하여 데이터 세트를 인위적으로 제한하는 것이 유용할 수 있습니다. 전체 쿼리가 만족스러우면 초기 take 단계를 제거할 수 있습니다.

데이터 정렬: sort / order

sort 연산자(및 동일한 order 연산자)는 지정된 열을 기준으로 데이터를 정렬하는 데 사용됩니다. 다음 예제에서는 TimeGenerated를 기준으로 결과를 정렬하고, desc 매개 변수를 통해 순서 방향을 내림차순으로 설정하여 가장 높은 값을 먼저 배치했습니다. 오름차순의 경우 asc를 사용합니다.

참고 항목

정렬의 기본 방향이 내림차순이므로 기술적으로는 오름차순으로 정렬하려는 경우에만 지정해야 합니다. 그러나 어떤 경우든지 정렬 방향을 지정하면 쿼리를 더 쉽게 읽을 수 있습니다.

SigninLogs
| sort by TimeGenerated desc
| take 5

앞서 설명한 대로 sort 연산자를 take 연산자 앞에 배치합니다. 적절한 5개 레코드를 가져오기 위해 먼저 정렬해야 합니다.

Screenshot of results of sort operator, with take limit.

top 연산자를 사용하면 sorttake 연산을 단일 연산자로 결합할 수 있습니다.

SigninLogs
| top 5 by TimeGenerated desc

정렬 기준으로 사용하는 열에서 둘 이상의 레코드 값이 같은 경우 정렬 기준으로 사용할 열을 더 추가할 수 있습니다. 쉼표로 구분된 목록에서 첫 번째 정렬 열 뒤, 정렬 순서 키워드 앞에 정렬 열을 추가합니다. 예시:

SigninLogs
| sort by TimeGenerated, Identity desc
| take 5

이제 TimeGenerated가 여러 레코드 간에 동일한 경우 Identity 열의 값을 기준으로 정렬하려고 합니다.

참고 항목

sorttake를 사용해야 하는 경우와 top을 사용해야 하는 경우

  • 하나의 필드에서만 정렬하는 경우 sorttake의 조합보다 더 나은 성능을 제공하는 top을 사용합니다.

  • 위의 마지막 예제와 같이 둘 이상의 필드에서 정렬해야 하는 경우 top으로 수행할 수 없으므로 sorttake를 사용해야 합니다.

데이터 필터링: where

where 연산자는 시나리오와 관련된 데이터 하위 집합만 사용하기 위한 관건이 되므로 가장 중요한 연산자라고 할 수 있습니다. 가능한 한 쿼리 초기에 데이터를 필터링하기 위해 최선을 다해야 합니다. 이렇게 하면 후속 단계에서 처리해야 하는 데이터 양이 감소하여 쿼리 성능이 향상되기 때문입니다. 또한 원하는 데이터에서만 계산을 수행하게 됩니다. 이 예제를 참조하세요.

SigninLogs
| where TimeGenerated >= ago(7d)
| sort by TimeGenerated, Identity desc
| take 5

where 연산자는 변수, 비교(스칼라) 연산자, 값을 지정합니다. 이 경우 >=를 사용하여 TimeGenerated 열의 값이 7일 전보다 이후거나 같아야 한다고 지정했습니다.

Kusto 쿼리 언어에는 문자열과 숫자라는 두 가지 유형의 비교 연산자가 있습니다. 다음 표에는 숫자 연산자의 전체 목록이 나와 있습니다.

숫자 연산자

Operator Description
+ 더하기
- 빼기
* 곱하기
/ 나누기
% 나머지
< 보다 작음
> 보다 큼
== 다음과 같음
!= 같지 않음
<= 작거나 같음
>= 크거나 같음
in 요소 중 하나와 같음
!in 요소와 같지 않음

문자열 연산자 목록은 대/소문자 구분, 하위 문자열 위치, 접두사, 접미사 등의 순열을 포함하므로 훨씬 더 긴 목록입니다. == 연산자는 숫자이자 문자열 연산자이므로 숫자와 텍스트에 모두 사용할 수 있습니다. 예를 들어 다음 문은 둘 다 유효한 where 문입니다.

  • | where ResultType == 0
  • | where Category == 'SignInLogs'

모범 사례: 대부분의 경우 둘 이상의 열을 기준으로 데이터를 필터링하거나 둘 이상의 방법으로 동일한 열을 필터링하려고 합니다. 이 경우 유의해야 할 두 가지 모범 사례가 있습니다.

and 키워드를 사용하여 여러 where 문을 단일 단계로 결합할 수 있습니다. 예시:

SigninLogs
| where Resource == ResourceGroup
    and TimeGenerated >= ago(7d)

위와 같이 and 키워드를 사용하여 여러 필터가 단일 where 문으로 조인된 경우 단일 열만 먼저 참조하는 필터를 배치하면 성능이 향상됩니다. 따라서 위 쿼리를 작성하는 더 좋은 방법은 다음과 같습니다.

SigninLogs
| where TimeGenerated >= ago(7d)
    and Resource == ResourceGroup

이 예제에서 첫 번째 필터는 단일 열(TimeGenerated)을 언급하고, 두 번째 필터는 두 개의 열(ResourceResourceGroup)을 참조합니다.

데이터 요약

Summarize는 Kusto 쿼리 언어에서 가장 중요한 테이블 형식 연산자 중 하나지만 일반적으로 쿼리 언어를 처음 사용하는 경우 배우기에 복잡한 연산자 중 하나기도 합니다. summarize 작업은 데이터 테이블을 받아서 하나 이상의 열로 집계된 새 테이블을 출력하는 것입니다.

summarize 문의 구조

summarize 문의 기본 구조는 다음과 같습니다.

| summarize <aggregation> by <column>

예를 들어 다음 코드는 Perf 테이블의 각 CounterName 값에 대한 레코드 수를 반환합니다.

Perf
| summarize count() by CounterName

Screenshot of results of summarize operator with count aggregation.

summarize의 출력이 새 테이블이므로 summarize 문에 명시적으로 지정되지 않은 열은 파이프라인 아래로 전달되지 않습니다. 개념 설명을 위해 다음 예제를 가정합니다.

Perf
| project ObjectName, CounterValue, CounterName
| summarize count() by CounterName
| sort by ObjectName asc

두 번째 줄에서 ObjectName,, CounterValue, CounterName 열에만 집중하도록 지정합니다. 그 다음에는 CounterName별 레코드 수를 가져오도록 요약했으며, 마지막으로 ObjectName 열의 오름차순으로 데이터를 정렬하려고 합니다. 그러나 요약할 때 CountCounterName 열만 새 테이블에 포함했기 때문에 이 쿼리는 실패하고 오류(ObjectName을 알 수 없음)가 발생합니다. 이 오류를 방지하기 위해 다음과 같이 summarize 단계의 끝에 ObjectName을 추가할 수 있습니다.

Perf
| project ObjectName, CounterValue , CounterName
| summarize count() by CounterName, ObjectName
| sort by ObjectName asc

summarize 줄을 읽는 방법은 “CounterName별로 레코드 수를 요약하고 ObjectName을 기준으로 그룹화”가 됩니다. summarize 문 끝에 열을 쉼표로 구분하여 계속 추가할 수 있습니다.

Screenshot of results of summarize operator with two arguments.

이전 예제를 기반으로 해서 여러 열을 동시에 집계하려는 경우 summarize 연산자에 집계를 쉼표로 구분해서 추가하면 됩니다. 아래 예제에서는 모든 레코드 수뿐만 아니라 쿼리의 필터와 일치하는 모든 레코드의 CounterValue 열 값 합계도 가져옵니다.

Perf
| project ObjectName, CounterValue , CounterName
| summarize count(), sum(CounterValue) by CounterName, ObjectName
| sort by ObjectName asc

Screenshot of results of summarize operator with multiple aggregations.

집계된 열 이름 바꾸기

이제 집계된 열의 열 이름에 대해 살펴보겠습니다. 이 섹션을 시작할 때summarize 연산자는 데이터 테이블을 받아서 새 테이블을 생성하고, summarize 문에 지정한 열만 파이프라인 아래로 전달된다고 말했습니다. 따라서 위 예제를 실행할 경우 결과로 생성되는 집계 열은 count_sum_CounterValue가 됩니다.

명시적으로 지정하지 않아도 Kusto 엔진에서 자동으로 열 이름을 만들지만, 새 열에 친숙한 이름을 사용하려는 경우가 많습니다. 다음과 같이 새 이름, =, 집계를 차례로 지정하면 summarize 문에서 열 이름을 쉽게 바꿀 수 있습니다.

Perf
| project ObjectName, CounterValue , CounterName
| summarize Count = count(), CounterSum = sum(CounterValue) by CounterName, ObjectName
| sort by ObjectName asc

이제 요약된 열의 이름이 CountCounterSum으로 바뀝니다.

Screenshot of friendly column names for aggregations.

summarize 연산자의 모든 기능을 여기서 설명할 수는 없지만, Microsoft Sentinel 데이터에서 수행하려는 모든 데이터 분석의 핵심 구성 요소이므로 시간을 할애해서 자세히 알아봐야 합니다.

집계 참조

많은 집계 함수가 있지만 가장 일반적으로 사용되는 몇 가지 함수는 sum(), count(), avg()입니다. 다음은 부분 목록입니다(전체 목록 참조).

집계 함수

함수 설명
arg_max() 인수가 최대화되면 하나 이상의 식을 반환합니다.
arg_min() 인수가 최소화되면 하나 이상의 식을 반환합니다.
avg() 그룹 전체의 평균 값을 반환합니다.
buildschema() 동적 입력의 모든 값을 허용하는 최소 스키마를 반환합니다.
count() 그룹의 개수를 반환합니다.
countif() 그룹의 조건자를 사용하여 개수를 반환합니다.
dcount() 그룹 요소의 대략적인 고유 개수를 반환합니다.
make_bag() 그룹 내의 동적 값에 대한 속성 모음을 반환합니다.
make_list() 그룹 내의 모든 값에 대한 목록을 반환합니다.
make_set() 그룹 내의 고유 값 세트를 반환합니다.
max() 그룹 전체의 최대값 반환
min() 그룹 전체의 최소값 반환
percentiles() 그룹에 대한 백분위수 근사치를 반환합니다.
stdev() 그룹 전체의 표준 편차를 반환합니다.
sum() 그룹 내 요소의 합계를 반환합니다.
take_any() 비어 있지 않은 임의의 그룹 값을 반환합니다.
variance() 그룹 전체의 분산을 반환합니다.

선택: 열 추가 및 제거

쿼리를 더 많이 사용하기 시작하면서 주제에 대해 보유한 정보가 필요한 것보다 많은 것을 발견할 수 있습니다(즉, 테이블의 열이 너무 많음). 또는 보유한 것보다 많은 정보가 필요할 수도 있습니다(즉, 다른 열의 분석 결과가 포함될 새 열을 추가해야 함). 열 조작을 위한 몇 가지 주요 연산자를 살펴봅시다.

Projectproject-away

Project는 많은 언어의 select 문과 거의 동일합니다. 이 연산자를 사용하여 유지할 열을 선택할 수 있습니다. 반환되는 열의 순서는 다음 예제와 같이 project 문에 나열된 열의 순서와 일치합니다.

Perf
| project ObjectName, CounterValue, CounterName

Screenshot of results of project operator.

상상할 수 있듯이 매우 넓은 데이터 세트로 작업하는 경우 유지할 열이 많을 수 있으며, 모든 열을 이름으로 지정하려면 많은 입력이 필요합니다. 이 경우 project-away를 사용하여 다음과 같이 유지할 열이 아닌 제거할 열을 지정할 수 있습니다.

Perf
| project-away MG, _ResourceId, Type

쿼리의 두 위치에서(시작 부분 및 끝 부분에서 다시) project를 사용하는 것이 유용할 수 있습니다. 쿼리 초기에 project를 사용하면 파이프라인 아래로 전달할 필요가 없는 큰 데이터 청크를 제거하여 성능을 향상할 수 있습니다. 끝 부분에서 다시 사용하면 이전 단계에서 만들었으며 최종 출력에 필요하지 않은 열을 제거할 수 있습니다.

확장

Extend는 새 계산 열을 만드는 데 사용됩니다. 이 기능은 기존 열에 대해 계산을 수행하고 모든 행의 출력을 확인하려는 경우에 유용할 수 있습니다. Kbytes라는 새 열을 계산하는 간단한 예제를 살펴봅시다. 이 열은 MB 값(기존 Quantity 열)에 1,024를 곱하여 계산할 수 있습니다.

Usage
| where QuantityUnit == 'MBytes'
| extend KBytes = Quantity * 1024
| project ResourceUri, MBytes=Quantity, KBytes

project 문의 최종 줄에서 각 열과 관련된 측정 단위를 쉽게 알 수 있도록 Quantity 열의 이름을 Mbytes로 바꾸었습니다.

Screenshot of results of extend operator.

extend가 기존 계산 열에서도 작동한다는 것에 유의합니다. 예를 들어 Kbytes에서 계산되는 Bytes라는 열을 하나 더 추가할 수 있습니다.

Usage
| where QuantityUnit == 'MBytes'
| extend KBytes = Quantity * 1024
| extend Bytes = KBytes * 1024
| project ResourceUri, MBytes=Quantity, KBytes, Bytes

Screenshot of results of two extend operators.

테이블 조인

Microsoft Sentinel의 작업은 대부분 단일 로그 유형을 사용하여 수행할 수 있지만, 데이터 간 상관 관계를 지정하거나 다른 데이터 세트에 대해 조회를 수행하려는 경우도 있습니다. 대부분의 쿼리 언어와 마찬가지로, Kusto 쿼리 언어는 다양한 유형의 조인을 수행하는 데 사용되는 몇 개의 연산자를 제공합니다. 이 섹션에서는 가장 많이 사용되는 연산자인 unionjoin을 살펴보겠습니다.

Union

Union은 둘 이상의 테이블을 받아서 모든 행을 반환합니다. 예시:

OfficeActivity
| union SecurityEvent

이 경우 OfficeActivitySecurityEvent 테이블의 모든 행이 반환됩니다. Union은 union의 동작을 조정하는 데 사용할 수 있는 몇 개의 매개 변수를 제공합니다. 가장 유용한 두 매개 변수는 withsourcekind입니다.

OfficeActivity
| union withsource = SourceTable kind = inner SecurityEvent

withsource 매개 변수를 사용하면 지정된 행의 값이 해당 행을 가져온 원본 테이블의 이름이 되는 새 열의 이름을 지정할 수 있습니다. 위 예제에서는 열 이름을 SourceTable로 지정했으며, 행에 따라 값은 OfficeActivity 또는 SecurityEvent가 됩니다.

지정한 다른 매개 변수는 kind로, 두 가지 옵션(inner 또는 outer)이 있습니다. 위 예제에서는 inner를 지정했습니다. 즉, 두 테이블에 모두 있는 열만 union 중에 유지됩니다. 또는 outer(기본값)를 지정한 경우 두 테이블의 모든 열이 반환됩니다.

Join

조인union과(와) 비슷하게작동하지만 테이블을 조인하여 새 테이블을 만드는 대신 을 조인하여 새 테이블을 만듭니다. 대부분의 데이터베이스 언어와 마찬가지로, 여러 유형의 조인을 수행할 수 있습니다. join의 일반적인 구문은 다음과 같습니다.

T1
| join kind = <join type>
(
               T2
) on $left.<T1Column> == $right.<T2Column>

join 연산자 뒤에 수행하려는 조인 종류와 여는 괄호를 차례로 지정합니다. 괄호 안에 조인할 테이블과 해당 테이블에 대해 추가할 다른 쿼리 문을 지정합니다. 닫는 괄호 뒤에 on 키워드를 사용한 다음, 왼쪽($left.<columnName> 키워드) 및 오른쪽($right.<columnName>) 열을 == 연산자로 구분해서 사용합니다. inner join의 예제는 다음과 같습니다.

OfficeActivity
| where TimeGenerated >= ago(1d)
    and LogonUserSid != ''
| join kind = inner (
    SecurityEvent
    | where TimeGenerated >= ago(1d)
        and SubjectUserSid != ''
) on $left.LogonUserSid == $right.SubjectUserSid

참고 항목

두 테이블에서 조인을 수행할 열의 이름이 같으면 $left$right를 사용할 필요가 없습니다. 대신, 열 이름만 지정하면 됩니다. 그러나 $left$right를 사용하는 것이 더 명시적이고 일반적으로 모범 사례로 간주됩니다.

참조용으로, 다음 표에는 사용 가능한 조인 유형 목록이 나와 있습니다.

조인 형식

조인 유형 설명
inner 두 테이블에서 일치하는 행의 각 조합에 대해 단일 값을 반환합니다.
innerunique 연결된 필드에 고유 값이 있고, 오른쪽 테이블에 일치 항목이 있는 왼쪽 테이블의 행을 반환합니다.
지정되지 않은 경우의 기본 조인 유형입니다.
leftsemi 오른쪽 테이블에 일치 항목이 있는 왼쪽 테이블의 모든 레코드를 반환합니다.
왼쪽 테이블의 열만 반환됩니다.
rightsemi 왼쪽 테이블에 일치 항목이 있는 오른쪽 테이블의 모든 레코드를 반환합니다.
오른쪽 테이블의 열만 반환됩니다.
leftanti/
leftantisemi
오른쪽 테이블에 일치 항목이 없는 왼쪽 테이블의 모든 레코드를 반환합니다.
왼쪽 테이블의 열만 반환됩니다.
rightanti/
rightantisemi
왼쪽 테이블에 일치 항목이 없는 오른쪽 테이블의 모든 레코드를 반환합니다.
오른쪽 테이블의 열만 반환됩니다.
leftouter 왼쪽 테이블의 모든 레코드를 반환합니다. 오른쪽 테이블에 일치 항목이 없는 레코드의 경우 셀 값은 Null이 됩니다.
rightouter 오른쪽 테이블의 모든 레코드를 반환합니다. 왼쪽 테이블에 일치 항목이 없는 레코드의 경우 셀 값은 Null이 됩니다.
fullouter 일치 여부와 관계없이 왼쪽 테이블과 오른쪽 테이블의 모든 레코드를 반환합니다.
일치하지 않는 값은 Null이 됩니다.

가장 작은 테이블을 왼쪽에 두는 것이 좋습니다. 이 규칙을 따르면, 수행하는 조인 유형 및 테이블 크기에 따라 성능이 훨씬 향상되는 경우도 있습니다.

Evaluate

첫 번째 예제로 돌아가서, 한 줄에 evaluate 연산자가 있는 것을 확인했습니다. evaluate 연산자는 앞서 설명한 연산자처럼 일반적으로 사용되지 않습니다. 그러나 evaluate 연산자의 작동 방식을 아는 것은 시간을 할애할 만한 가치가 있습니다. 첫 번째 쿼리는 다음과 같으며, 두 번째 줄에 evaluate가 표시됩니다.

SigninLogs
| evaluate bag_unpack(LocationDetails)
| where RiskLevelDuringSignIn == 'none'
   and TimeGenerated >= ago(7d)
| summarize Count = count() by city
| sort by Count desc
| take 5

이 연산자를 통해 사용 가능한 플러그 인(기본적으로 기본 제공 함수)을 호출할 수 있습니다. 대부분의 플러그 인은 autocluster, diffpatterns, sequence_detect와 같은 데이터 과학을 중점으로 하므로 플러그 인을 사용하여 고급 분석을 수행하고 통계 변칙 및 이상값을 발견할 수 있습니다.

위 예제에서 사용된 플러그 인은 bag_unpack이라고 하며, 쉽게 동적 데이터 청크를 받아서 열로 변환할 수 있습니다. 동적 데이터는 다음 예제와 같이 JSON과 매우 유사한 데이터 형식입니다.

{
"countryOrRegion":"US",
"geoCoordinates": {
"longitude":-122.12094116210936,
"latitude":47.68050003051758
},
"state":"Washington",
"city":"Redmond"
}

이 경우, 도시별로 데이터를 요약하려고 했지만 cityLocationDetails 열에 속성으로 포함되었습니다. 쿼리에서 city 속성을 사용하기 위해 먼저 bag_unpack을 사용하여 열로 변환해야 했습니다.

원래 파이프라인 단계로 돌아갔을 때 다음과 같이 표시되었습니다.

Get Data | Filter | Summarize | Sort | Select

이제 evaluate 연산자를 고려했으므로 파이프라인의 새 단계를 표시하는 것을 확인할 수 있습니다. 다음과 같이 표시됩니다.

Get Data | Parse | Filter | Summarize | Sort | Select

데이터 원본을 읽기 쉽고 조작 가능한 형식으로 구문 분석하는 데 사용할 수 있는 연산자 및 함수의 다른 많은 예제가 있습니다. 전체 설명서통합 문서에서 해당 연산자 및 함수와 Kusto 쿼리 언어의 나머지 부분에 대해 알아볼 수 있습니다.

let 문

많은 주요 연산자와 데이터 형식을 살펴보았으므로, 이제 쉽게 읽고, 편집하고, 유지 관리할 수 있는 쿼리를 만드는 좋은 방법인 let으로 마무리합시다.

Let을 사용하면 변수를 만들고 설정하거나 식에 이름을 할당할 수 있습니다. 이 식은 단일 값일 수 있지만, 전체 쿼리일 수도 있습니다. 다음은 간단한 예제입니다.

let aWeekAgo = ago(7d);
SigninLogs
| where TimeGenerated >= aWeekAgo

여기서는 이름을 aWeekAgo로 지정했으며, datetime 값을 반환하는 timespan 함수의 출력과 같도록 설정했습니다. 그런 다음, let 문을 세미콜론으로 종료합니다. 이제 쿼리의 어디에서나 사용할 수 있는 aWeekAgo라는 새 변수가 있습니다.

방금 설명한 것처럼, let 문을 사용하여 전체 쿼리를 받고 결과에 이름을 지정할 수 있습니다. 테이블 형식 식인 쿼리 결과를 쿼리의 입력으로 사용할 수 있으므로, 다른 쿼리를 실행하기 위해 이 명명된 결과를 테이블로 처리할 수 있습니다. 다음은 이전 예제를 약간 수정한 것입니다.

let aWeekAgo = ago(7d);
let getSignins = SigninLogs
| where TimeGenerated >= aWeekAgo;
getSignins

이 경우 전체 쿼리를 getSignins라는 새 변수로 래핑하는 두 번째 let 문을 만들었습니다. 이전과 마찬가지로, 두 번째 let 문을 세미콜론으로 종료합니다. 그런 다음, 최종 줄에서 변수를 호출하여 쿼리를 실행합니다. 두 번째 let 문에 aWeekAgo를 사용할 수 있었습니다. 이전 줄에서 지정했기 때문입니다. getSignins가 앞에 오도록 let 문을 교환하면 오류가 발생합니다.

이제 getSignins를 다른 쿼리의 토대로 사용할 수 있습니다(동일한 창).

let aWeekAgo = ago(7d);
let getSignins = SigninLogs
| where TimeGenerated >= aWeekAgo;
getSignins
| where level >= 3
| project IPAddress, UserDisplayName, Level

Let 문은 쿼리를 구성하는 데 도움이 되는 더 많은 기능과 유연성을 제공합니다. Let은 스칼라 및 테이블 형식 값을 정의하고 사용자 정의 함수를 만들 수 있습니다. 여러 조인을 수행할 수 있는 더 복잡한 쿼리를 구성할 때 유용합니다.

다음 단계

이 문서에서는 기본 사항만 간략하게 살펴보았지만 이제 필요한 토대가 구축되었으며, Microsoft Sentinel에서 작업을 수행하기 위해 가장 자주 사용할 부분을 설명했습니다.

Microsoft Sentinel용 고급 KQL 통합 문서

Microsoft Sentinel 자체에서 바로 Kusto 쿼리 언어 통합 문서인 Microsoft Sentinel용 고급 KQL 통합 문서를 활용합니다. 일상적인 보안 작업 중에 발생할 수 있는 많은 상황에 대한 단계별 도움말 및 예제를 제공하고 분석 규칙, 통합 문서, 헌팅 규칙 및 Kusto 쿼리를 사용하는 더 많은 요소의 즉시 사용 가능한 많은 기본 제공 예제도 가리킵니다. Microsoft Sentinel의 통합 문서 블레이드에서 이 통합 문서를 시작합니다.

고급 KQL Framework 통합 문서 - KQL 전문가 되기는 이 통합 문서를 사용하는 방법을 보여 주는 훌륭한 블로그 게시물입니다.

추가 리소스

Kusto 쿼리 언어에 대한 지식을 넓히고 심화하는 방법은 학습, 교육, 기술 리소스 컬렉션을 참조하세요.