# 예약 검색 예시

이 페이지에는 로그에서 의심스러운 활동을 조사하는 동안 사용할 수 있는 일반적인 사용 사례와 예시 검색이 포함되어 있습니다.

아래 예시는 효과적으로 사용하기 위해 로컬 환경에 맞는 일부 사용자 지정이 필요합니다. 모든 쿼리는 결과 크기를 제어해야 한다는 점에 유의하세요. 이는 다음을 사용하여 수행할 수 있습니다. `LIMIT` 또는 `GROUP BY` 절.

{% hint style="warning" %}
예약된 검색이 실행될 때마다 귀사의 데이터 플랫폼에 비용이 발생합니다. 쿼리가 지정된 시간 초과 기간 내에 완료될 수 있도록 반드시 확인하세요.
{% endhint %}

### 스트리밍 룰

Panther를 사용하면 다음을 실행할 수 있습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) ([Saved Searches ](https://docs.panther.com/ko/search/scheduled-searches)간격으로) 그리고 Scheduled Rule과 함께 사용하면, 긴 시간 범위에 걸쳐 작동하고 여러 소스의 데이터를 집계하는 디택션을 생성할 수 있습니다.

가능한 경우 스트리밍 룰을 사용하여 디택션을 구현해 보세요. 스트리밍 룰은 Scheduled Search에 비해 지연 시간이 짧고 비용도 더 적게 듭니다. 그러나 디택션에 더 많은 컨텍스트가 필요한 경우가 있으며, 그럴 때는 Scheduled Search가 적절한 해결책입니다.

### 데이터 지연 시간 및 타이밍 고려 사항

효과적인 Scheduled Rule을 작성하려면 이벤트가 기록된 시점부터 Panther에 도달할 때까지 데이터의 지연 시간을 이해해야 합니다. 이 정보를 사용하여 일정과 사용되는 시간 창을 그에 맞게 조정하세요.

예를 들어 AWS CloudTrail 데이터는 약 10분의 지연 시간이 있습니다. 이는 Panther 기능 때문이 아니라 AWS의 제한 때문입니다. 지난 1시간의 데이터를 분석하려는 경우, 모범 사례로 정시 후 15분에 검색이 실행되도록 예약하는 것을 권장합니다.

이는 Panther 스트리밍 rules에는 해당되지 않습니다. 데이터가 시스템으로 들어와 처음 처리될 때 적용되기 때문입니다. Scheduled Search는 누적된 데이터를 주기적으로 되돌아보므로, 타이밍 고려 사항이 중요합니다.

## 예시

### AWS 콘솔 액세스를 특정 IP 주소로 제한

Scheduled Search와 Scheduled Rule을 사용하여 디택션을 생성하는 과정을 더 잘 이해하기 위해 매우 간단한 엔드투엔드 예시부터 시작해 보겠습니다. 귀사가 회사 IP 공간의 IP 주소에서만 AWS 콘솔에 대한 액세스를 매우 신중하게 제한한다고 가정해 보겠습니다. 이 제어를 검증하기 위해 모든 AWS 콘솔 로그인에 다음이 있는지 확인하려고 합니다. `sourceIPAddress` 이 IP 공간 내에서. 귀하는 datalake에 이러한 IP 블록의 테이블을 보관합니다. 참고: 이 예시는 단순한 동등 조인 작업을 사용하며 IP 블록 테이블이 모두 /32 주소라고 가정합니다. 현실적인 구현에서는 IP 블록 테이블이 CIDR 블록을 가지며, 검사는 CIDR 블록 내부에 있지 않은 항목을 찾는 것이 됩니다. 이는 독자의 연습 과제로 남겨 둡니다.

CloudTrail과 관련된 데이터의 고유한 지연을 처리하기 위해 긴 윈도우를 사용하여, 이전 30분을 확인하면서 15분마다 실행되도록 룰을 예약한다고 가정해 보겠습니다.

전체 공개: 귀하는 *할 수 있습니다* IP 화이트리스트 테이블을 Dynamodb 또는 S3에서 관리했다면 스트리밍 룰을 사용하여 이 디택션을 구현할 수 있습니다. 로그 소스의 볼륨이 매우 큰 경우 아래와 같이 주기적인 배치 조인을 수행하는 것이 더 효율적입니다.

이러한 이벤트를 디택션하는 쿼리는 다음과 같습니다:

{% tabs %}
{% tab title="SQL" %}

```sql
SELECT
    ct.*
FROM
    panther_logs.public.aws_cloudtrail ct LEFT OUTER JOIN infrastructure.networking.egress_blocks egress
        ON (ct.sourceIPAddress = egress.ip)
WHERE
    p_occurs_since('30 minutes') 
    AND
    ct.eventtype = 'AwsConsoleSignIn'
    AND 
    egress.ip IS NULL -- 일치하지 않음!
LIMIT 1000 -- 이런 경우가 많을 것으로 예상하지는 않지만 안전을 위해 여기에 둡니다!
```

다음에 유의하세요. `p_occurs_since()` 는 Panther의 [SQL 매크로](https://docs.panther.com/ko/search/data-explorer) 이며 Scheduled Query를 더 쉽게 만들 수 있도록 합니다.
{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
panther_logs.public.aws_cloudtrail
| where p_event_time > time.ago(30m) 
    and eventtype == 'AwsConsoleSignIn' 
| join kind=leftouter egress=(infrastructure.networking.egress_blocks) 
     on $left.sourceIPAddress == $right.ip
| where egress.ip == null
| limit 1000
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Scheduled Search의 출력은 Scheduled Rule을 통해 흐르므로(Python에서) 반환되는 행 수를 신중하게 제어하는 것이 중요합니다. 다음을 *항상* 제공하거나 `LIMIT` 절을 제공하거나 `GROUP BY` 행 수가 제한된 집계(최대 수천 개 미만)를 사용하는 것이 권장됩니다.
{% endhint %}

이를 구현하려면:

1. 예약된 검색 만들기 [다음 지침을 따라](https://docs.panther.com/ko/search/scheduled-searches/..#how-to-create-a-scheduled-search).
   * 아래 예시 스크린샷에서는 30분마다의 주기가 선택되어 있습니다.\
     ![The image shows the query creation form. It contains fields for Query Name, Tags, and Description. The name is set to "Sketchy AWS Console Logins."The option "Period" is selected, and a dropdown labeled "Period (min)" is set to 30.](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-a1770f1174a73ae684642a826c428f6f7335ff67%2Fquery-aws-example.png?alt=media)
2. Scheduled Search가 활성 상태로 설정되어 있는지 확인하세요.
3. 다음을 만드세요 [예약된 룰](https://docs.panther.com/ko/detections/rules) Scheduled Search의 출력물을 대상으로 합니다.\
   ![The image shows the Scheduled Rule creation page. The "Scheduled Queries" dropdown is set to "Sketchy AWS Console Logins."](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-532e83af3860a1259e89ecd0119f590cb8a428b8%2Fquery-aws-example-rule.png?alt=media)![An example Python rule is written under "Rule Function" in the Scheduled Rule creation page.](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-ff69cf6f5840cba7d17e8eb84128d26e9f8d8a61%2Fquery-aws-example-function.png?alt=media)

Scheduled Rule은 streaming 룰의 모든 기능을 갖추고 있어, 알러트를 사용자 지정하고 대상을 직접 지정할 수 있습니다. Panther의 deduping은 알러트 폭주를 방지하며, 위의 룰에서는 다음을 사용합니다. `sourceIPAddress` dedupe는 30분당 1개의 알러트만 생성합니다.

목록과 조인하는 이 패턴은 IOC 디택션에도 사용할 수 있습니다(TOR Exit Nodes, malware hashes 등과 같은 IOC 테이블 유지).

### Command and Control (C2) beacon 디택션

이 예시에서는 집계를 사용해 C2 beaconing을 찾는 매우 단순하지만 효과적인 행위 기반 디택션을 만들겠습니다.

{% hint style="info" %}
이것은 설명 목적만을 위한 지나치게 단순화된 디택션입니다. allowlisting 및 임계값 조정 같은 개선 없이 이를 사용하면 과도한 오탐이 발생할 수 있습니다(“beacon”하는 비악성 프로세스가 많이 있습니다). 그렇지만 잘 이해된 네트워크에서 적절한 allowlisting을 사용하면 이 기법은 매우 효과적일 수 있습니다.
{% endhint %}

우리는 C2 beacon을 다음과 같은 모든 IP 활동으로 정의하겠습니다. *최대* 하루에 다섯 번 발생하고, 3일 이상 반복됩니다. 이를 구현하려면:

{% tabs %}
{% tab title="SQL" %}

```sql
WITH low_and_slow_daily_ip_activity AS (
SELECT
    date(p_event_time) as day,
    srcAddr as beacon_ip
FROM
    panther_logs.public.aws_vpcflow
WHERE
    p_occurs_since('1 week')
GROUP BY 1,2
HAVING count(1) <= 5 -- 활동이 거의 없는 경우만, 이는 튜닝이 필요합니다
)
SELECT
 beacon_ip,
 count(1) as days
FROM
 low_and_slow_daily_ip_activity
GROUP BY 
 1
HAVING days >= 3 -- 최소 이 일수 이상 활동이 있는 경우만
LIMIT 20 -- 알러트 폭주를 피하세요!
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
panther_logs.public.aws_vpcflow
| where p_event_time > time.ago(7d)
| summarize count=agg.count() by day=time.trunc('d', p_event_time), beacon_ip=srcAddr
| where count <= 5  // 활동이 거의 없는 경우만, 이는 튜닝이 필요합니다
| summarize days=agg.count() by beacon_ip
| where days >= 3   // 최소 이 일수 이상 활동이 있는 경우만
| limit 20          // 알러트 폭주를 피하세요!
```

{% endtab %}
{% endtabs %}

이를 구현하려면:

1. 예약된 검색 만들기 [다음 지침을 따라](https://docs.panther.com/ko/search/scheduled-searches/..#how-to-create-a-scheduled-search).
   * 아래 예시 스크린샷에서는 Cron expression을 사용해 이것이 매일 자정 1분 후에 실행되도록 합니다.\
     ![The query creation screen is open, and the Query Name is set to "C2 Beacons." The option "Cron Expression" is selected. Minutes is set to 1, and Timeout is set to 1.](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-1842acd511de636012c5fe423b6d1e23d13ce893%2Fc2-beacon-example.png?alt=media)
2. Scheduled Search가 활성 상태로 설정되어 있는지 확인하세요.
3. 다음을 만드세요 [예약된 룰](https://docs.panther.com/ko/detections/rules) Scheduled Search의 출력물을 대상으로 합니다:\
   ![The Scheduled Rule creation screen is displayed. The "scheduled queries" dropdown is set to "C2 Beacons."](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-f089f03db9886b029bc63e49445344950a29339d%2Fc2-beacon-rule.png?alt=media)![An example Python rule is written in the "Rule Function" text box on the Scheduled Rule creation page.](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-3c68817fa2926a476f3d9d1de8644c00ca150f33%2Fc2-beacon-query.png?alt=media)

### 내 endpoint monitoring은 얼마나 잘 작동하고 있나요?

이 가상의 예시에서는 endpoint monitoring 소프트웨어로 CrowdStrike를 사용한다고 가정하겠습니다. Panther는 로그를 수집하도록 구성되어 있으며, 다음이 있습니다. [CMDB](https://en.wikipedia.org/wiki/Configuration_management_database) 배포된 agent를 내부적으로 연결된 사용자와 매핑하도록 채워져 있습니다.

여기에는 *많은* 흥미로운 질문들이 있지만, 이 예시에서는 구체적으로 다음 질문을 하겠습니다: "지난 24시간 동안 ANY 데이터도 보고하지 않은 endpoint는 무엇인가?"

CrowdStrike 로그에서 배포된 agent의 고유 id는 다음과 같이 불립니다. `aid` . CMDB에는 다음의 매핑이 있습니다. `aid` 를 reference data에 매핑합니다. 이 예시에서는 여기에 다음 속성이 있다고 가정합니다. `employee_name`, `employee_group` 및 `last_seen`. 직원 관련 속성은 현재 누가 해당 endpoint를 사용하는지 식별하는 데 도움이 되며, `last_seen` 는 네트워크 활동(예: VPN access, DHCP leases, Authentication, liveliness 디택션 등)을 추적하는 백엔드 프로세스에 의해 업데이트된다고 가정하는 타임스탬프입니다.

이 질문에 답하려면, CMDB에 있는 agent 중 지난 24시간 동안 *실제로* 네트워크 활동은 있지만 CrowdStrike 활동은 *표시되지 않지만* 전혀 없는 경우를 알고 싶습니다. 이는 agent가 실행되고 있지 않거나 비활성화되었음을 나타낼 수 있습니다(coverage gap을 의미). 아래 쿼리는 특정 의심 endpoint를 포함한 employee group별 보고서를 계산합니다:

{% tabs %}
{% tab title="SQL" %}

```sql
WITH active_aids AS (
SELECT
     DISTINCT aid -- 고유한 agent id만 일치
FROM panther_logs.public.crowdstrike_aidmaster
WHERE p_occurs_since('1 day') -- 로그 데이터의 최근 24시간 내
)
SELECT
    cmdb.employee_group,
    count(1) AS num_inactive_endpoints,
    ARRAY_AGG(
        DISTINCT cmdb.aid || ' ' || cmdb.employee_name || ' ' || cmdb.employee_group
    ) as endpoints -- 여기서는 특정 엔드포인트를 수집합니다
FROM 
  infrastructure.inventory.cmdb AS cmdb LEFT OUTER JOIN active_aids cs USING(aid)
WHERE
  cs.aid IS NULL -- 어떤 로그 데이터와도 일치하지 않음
AND 
  cmdb.last_seen BETWEEN current_date - 1 AND current_date -- 활성 상태일 가능성이 높음
GROUP BY 1
ORDER BY 2 desc
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
let active_aids = panther_logs.public.crowdstrike_aidmaster
| where p_event_time > time.ago(1d)
| summarize by aid;

infrastructure.inventory.cmdb
| where last_seen > time.ago(1d)
| join kind=leftouter aa=(active_aids) on $left.aid == $right.aid
| where aid == null
| extend endpoints=strings.cat(aid, ' ', employee_name, ' ', employee_group)
| summarize num_inactive_endpoints=agg.count(),
            endpoints=agg.make_set(endpoints) by employee_group 
| sort num_inactive_endpoints
```

{% endtab %}
{% endtabs %}

이를 구현하려면:

1. 예약된 검색 만들기 [다음 지침을 따라](https://docs.panther.com/ko/search/scheduled-searches/..#how-to-create-a-scheduled-search).
   * 아래 예시 스크린샷에서는 Cron expression을 사용해 이것이 매일 자정 1분 후에 실행되도록 합니다.\
     ![The query creation page is displayed. The query name is "Inactive Crowdstrike Endpoints." The option "Cron expression" is selected. Minutes is set to 1 and Timeout is set to 1.](https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-0037cba28bdc6616635e39a26d096b9631771439%2Fcrowdstrike-query-example.png?alt=media)
2. Scheduled Search가 활성 상태로 설정되어 있는지 확인하세요.
3. 다음을 만드세요 [예약된 룰](https://docs.panther.com/ko/detections/rules) Scheduled Search의 출력물을 대상으로 합니다:

<figure><img src="https://2400888838-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LgdiSWdyJcXPahGi9Rs-2910905616%2Fuploads%2Fgit-blob-cd57e14eb0199aea897fe817050a2385e764166a%2Fcrowdstrike-rule.png?alt=media" alt="The image shows an example function written in Python. The Test box is open and has an example test written in it." width="563"><figcaption><p>CrowdStrikeRule</p></figcaption></figure>

알러트와 관련된 이벤트는 분석가가 검토할 수 있으며, 이는 직원 그룹당 최대 하나가 됩니다. "hits"는 다음에 누적됩니다 `endpoints` 직원 정보를 사용하여 쉽게 검증할 수 있습니다. 모든 Panther 규칙과 마찬가지로 알러트 대상의 목적지를 유연하게 사용자 지정할 수 있습니다. 예를 들어, `employee_group` 은 `C-Suite` 라면 아마도 온콜에게 페이지가 생성되고, 기본 알러트는 단순히 다음 날 검토를 위한 작업 대기열로 전달됩니다.

### 비정상적인 Okta 로그인

다음 [Okta 로그](https://docs.panther.com/ko/data-onboarding/supported-logs/okta) 는 이벤트와 관련된 "누가", "어떤 장치로", "어디에서"라는 질문에 대한 답을 제공합니다. 이 정보는 예를 들어 도난된 자격 증명을 사용하는 공격자와 같은 의심스러운 행동을 식별하는 데 사용할 수 있습니다.

과제는 "의심스러운" 것을 정의하는 것입니다. 의심스러움을 정의하는 한 가지 방법은 정상 상태에서의 편차입니다. 각 사용자에 대한 기준선을 만들 수 있다면, 유의미한 변화가 있을 때 알러트할 수 있습니다.

좋습니다. 하지만 이제는 많은 오탐 없이 유용한 보안 결과를 생성하는 방식으로 "유의미한 변화"를 정의해야 합니다. 이 예시에서는 다음에 대한 유의미한 변경을 대상으로 합니다. `클라이언트` 도난된 자격 증명을 나타낼 수 있는 정보. 참고: Okta 데이터는 맥락 정보가 매우 풍부하며, 이것은 이 데이터를 활용하는 한 가지 간단한 예시일 뿐입니다.

VPN과 프록시 때문에, 의심스러운 활동을 식별하기 위해 특정 IP 주소나 관련 지리 정보를 단순히 사용하는 것은 종종 실용적이지 않습니다. 마찬가지로, 사용자는 새 장치를 사용하기 때문에 장치를 바꿀 수도 있고, 여러 장치를 사용할 수도 있습니다. 우리는 정상 사용자 사이에 상당한 변동이 있을 것으로 예상합니다. 그러나 특정 사용자에 대해서는 시간이 지남에 따라 더 많은 일관성이 있을 것으로 기대합니다.

이 예시에서는 각 `행위자`에 대해 과거 최대 30일 동안 다음을 계산하여 "정상"을 특징짓겠습니다:

* 사용된 고유 인증 클라이언트
* 사용된 고유 OS 버전
* 사용된 고유 장치
* 사용된 고유 위치(정의: 국가, 주, 도시)

네 가지 차원 중 어떤 것과도 일치하지 않는 이벤트를 "의심스러운" 것으로 정의하겠습니다. 이는 다음을 의미합니다:

* 새 장치를 받더라도 알러트가 발생하지 않습니다.
* 위치가 바뀔 때도 알러트가 발생하지 않습니다.
* 우리는 *알러트를* 모든 속성이 한 번에 바뀔 때 받게 됩니다,

  그리고 우리는 이것이 보안 관점에서 비정상적이면서도 흥미롭다고 가정합니다.

또한 새 직원으로 인한 오탐을 피하기 위해, 최소 5일의 이력이 있는 행위자만 고려할 것입니다.

이 작업을 이전 하루에 대해 하루에 한 번 실행하도록 예약한다고 가정합니다.

이것은 단지 예시이며, 다른 휴리스틱과 마찬가지로 조정이 필요하지만, 행위자별로 자체 보정된다는 장점이 있습니다.

위 내용을 계산하는 SQL은 다음과 같습니다:

{% tabs %}
{% tab title="SQL" %}

```sql
WITH actor_baseline AS (
  SELECT
    actor:id as actor_id,
    ARRAY_AGG (DISTINCT client:id) as client_id_list,
    ARRAY_AGG (DISTINCT client:userAgent.os)  as client_os_list,
    ARRAY_AGG (DISTINCT client:device) as client_devices_list,
    ARRAY_AGG (DISTINCT 
       client:geographicalContext:country || client:geographicalContext:state || client:geographicalContext:city)
      as client_locations_list
  FROM
    panther_logs.public.okta_systemlog
  WHERE
    p_occurs_since('30 days') 
  GROUP BY 1
  HAVING
    COUNT(DISTINCT date(p_event_time)) > 5 -- 최소 5일의 이력
)
-- 현재 날짜를 기준선과 비교하여 기준선 속성 중 어떤 것과도 일치하지 않는 이벤트를 반환
SELECT
  logs.*
FROM
  panther_logs.public.okta_systemlog logs JOIN actor_baseline bl ON (actor:id = bl.actor_id)
WHERE
  p_occurs_since('1 day') 
  AND
  NOT ARRAY_CONTAINS(logs.client:id::variant, bl.client_id_list)
  AND
  NOT ARRAY_CONTAINS(logs.client:userAgent:os::variant, bl.client_os_list)
  AND
  NOT ARRAY_CONTAINS(logs.client:device::variant, bl.client_devices_list)
  AND
  NOT ARRAY_CONTAINS(
          (client:geographicalContext:country || 
           client:geographicalContext:state || 
           client:geographicalContext:city)::variant, bl.client_locations_list)
```

검색이 실행될 때마다 이러한 기준선을 다시 계산하는 것은 그다지 효율적이지 않습니다. 앞으로 Panther는 위에서 설명한 방법을 더 효율적으로 만들 수 있도록 요약 테이블을 생성하는 기능을 지원할 예정입니다.
{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
let actor_baseline = panther_logs.public.okta_systemlog
| where p_event_time > time.ago(30d)
| summarize unique_days=agg.count_distinct(time.trunc('d', p_event_time)),
            client_id_list=agg.make_set(client.id),
            cliend_os_list=agg.make_set(client.userAgent.os),
            client_devices_list=agg.make_set(client.device),
            client_locations_list=agg.make_set(strings.cat(client.geographicalContext.country, 
                                                           client.geographicalContext.state, 
                                                           client.geographicalContext.city)) by actor_id=actor.id
| where unique_days > 5;

panther_logs.public.okta_systemlog
| where p_event_time > time.ago(1d)
| join kind=inner ab=(actor_baseline) on $left.actor.id == $right.actor_id
| where client.id not in ab.client_id_list
    and client.userAgent.os not in ab.client_os_list
    and client.device not in ab.client_devices_list
    and strings.cat(client.geographicalContext.country, 
                    client.geographicalContext.state, 
                    client.geographicalContext.city) not in ab.client_locations_list
```

{% endtab %}
{% endtabs %}

### 비밀번호 스프레이 공격 탐지

비밀번호 스프레이는 흔히 사용되는 몇 가지 비밀번호로 수많은 계정(사용자 이름)에 접근을 시도하는 공격입니다. 전통적인 무차별 대입 공격은 비밀번호를 추측하여 단일 계정에 무단 접근을 시도합니다. 이는 일반적으로 사용되는 계정 잠금 정책이 정해진 시간 동안 제한된 횟수의 실패 시도(보통 3\~5회)만 허용하기 때문에, 대상 계정이 빠르게 잠길 수 있습니다. 비밀번호 스프레이 공격(“low-and-slow” 방식이라고도 함)에서는 악의적 행위자가 ‘password123’ 또는 ‘winter2017’과 같이 흔히 사용되는 단일 비밀번호를 여러 계정에 시도한 뒤, 두 번째 비밀번호를 시도하는 방식으로 계속 진행합니다. 이 기법은 빠르거나 잦은 계정 잠금을 피함으로써 행위자가 탐지되지 않도록 해 줍니다.

이러한 행동을 탐지하는 핵심은 시간을 두고 집계하여 실패한 로그인에서 사용자 이름의 다양성을 살펴보는 것입니다. 아래 예시는 CloudTrail을 사용하지만, 유사한 기법은 어떤 인증 로그에도 사용할 수 있습니다. 선택한 임계값은 대상 네트워크에 맞게 조정해야 합니다.

{% tabs %}
{% tab title="SQL" %}

```sql
SELECT
  -- 이 정보는 알러트 이벤트에 포함됩니다
  awsRegion as region,
  recipientAccountId를 accountid로,
  COUNT(DISTINCT useridentity:userName)을 distinctUserNames로,
  COUNT(1)을 failures로,
  MIN(p_event_time)을 first_attempt로,
  MAX(p_event_time)을 last_attempt로
FROM
  panther_logs.public.aws_cloudtrail
WHERE
  -- 이것은 3600초(1시간)를 되돌아보는 Panther 매크로입니다
  p_occurs_since('1 hour') 
  AND
  eventtype = 'AwsConsoleSignIn'
  AND
  responseElements:ConsoleLogin = 'Failure'
GROUP BY
  region, accountid
HAVING
  distinctUserNames > 5
   AND
  failures > 10
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
panther_logs.public.aws_cloudtrail
| where p_event_time > time.ago(1h)
    and eventType == 'AwsConsoleSignIn'
    and responseElements.ConsoleLogin == 'Failure'
| summarize distinctUserNames=agg.count_distinct(useridentity.userName),
            failures=agg.count(),
            first_attempt=agg.min(p_event_time),
            last_attempt=agg.max(p_event_time) by region=awsRegion, accountid=recipientAccountId
| where distinctUserNames > 5 and failures > 10
```

{% endtab %}
{% endtabs %}

### DNS 터널 탐지

대부분의 네트워크에서는 DNS를 일반적으로 차단할 수 없기 때문에, DNS 기반 데이터 유출 및 C2는 매우 효과적일 수 있습니다. DNS 기반 터널을 만들 수 있는 도구는 많이 있습니다. 아이러니하게도 모든 DNS 터널이 악의적인 것은 아니며, 많은 안티바이러스 도구가 원격지로 원격측정 데이터를 보내기 위해 DNS 터널을 사용합니다. 보안에 민감한 많은 사람들은 DNS 터널을 불안하게 여기므로, 네트워크에서 이를 탐지하는 것은 유용합니다. 간단한 트래픽 분석만으로도 이러한 터널을 쉽게 찾을 수 있지만, 합법적인 터널도 있기 때문에 아래 예시는 임계값과 허용 목록 모두에 대해 로컬 환경에 맞춘 조정이 필요합니다.

DNS 터널의 잠재적 기준은 1시간 동안 충분한 데이터를 전송하면서, 매우 적은 수의 고유 도메인으로만 이동하는 DNS 서버(포트 53)로 정의합니다.

이러한 터널을 식별하기 위해 매시간 이 검색을 실행하면서 1시간 전까지를 되돌아본다고 가정합니다:

{% tabs %}
{% tab title="SQL" %}

```sql
SELECT
  account_id,
  region,
  vpc_id,
  srcAddr, -- 외부
  srcIds:instance, -- 내부

  COUNT(1) as message_count,
  ARRAY_AGG(DISTINCT query_name) as query_names
FROM
  panther_logs.public.aws_vpcdns
WHERE
  p_occurs_since('1 hour')
  AND
  -- 간단한 허용 목록
  query_name NOT LIKE '%amazonaws.com'
GROUP BY
  1,2,3,4,5
HAVING
  message_count >= 1000   -- 1시간 동안 상당한 양의 활동
   AND
  ARRAY_SIZE(query_names) <= 2 -- 고유 도메인이 아주 적음(실제 DNS 서버일 가능성은 낮음!)
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
panther_logs.public.aws_vpcdns
| where p_event_time > time.ago(1h) and not strings.like(query_name, '%amazonaws.com')
| summarize message_count=agg.count(), 
            query_names=agg.make_set(query_name) by account_id,
                                                    region,
                                                    vpc_id,
                                                    srcAddr,         // 외부
                                                    srcIds.instance, // 내부
| where message_count >= 1000        // 1시간 동안 상당한 양의 활동
    and arrays.len(query_names) <=2  // 고유 도메인이 아주 적음(실제 DNS 서버일 가능성은 낮음!)
```

{% endtab %}
{% endtabs %}

### 클라우드 인프라의 월간 보고

Panther가 [Cloud Security](https://docs.panther.com/ko/cloud-scanning) AWS 인프라에 대해 보고할 수 있으므로, 다음을 사용할 수 있습니다. `resource_history` 운영과 보안 모두에 관심이 있을 수 있는 활동 통계를 계산하기 위한 테이블입니다.

간단한 예로, 아래 보고서는 매월 1일에 지난달에 대해 예약 실행되어 모니터링된 계정의 활동을 보여줄 수 있습니다.

{% tabs %}
{% tab title="SQL" %}

```sql
WITH monthly_report AS (
  SELECT
    accountId,
    changeType,
    ARRAY_AGG(DISTINCT resourceType) as resourceTypes,
    count(1) as objects
  FROM
    panther_cloudsecurity.public.resource_history
  WHERE
      DATE_TRUNC('MONTH', p_event_time) = DATE_TRUNC('MONTH', current_date - 1)  -- 1일에 실행하므로 지난달 전체
    AND
    changeType <> 'SYNC' -- 이것들은 변경 사항이 아니라 전체 레코드입니다
  GROUP BY 1,2
)
-- 전체 보고서를 Python 룰로 전달할 수 있는 JSON 객체의 단일 행으로 만들고자 합니다
SELECT
  ARRAY_AGG(OBJECT_CONSTRUCT(*)) as monthly_report
FROM monthly_report
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
let monthly_report = panther_cloudsecurity.public.resource_history
// TODO: 날짜에서 빼기
| where time.trunc('month', p_event_time) == time.trunc('month', time.now()-1d)
    and changeType != 'SYNC' // 이것들은 변경 사항이 아니라 전체 레코드입니다
| summarize resourceTypes=agg.make_set(resourceType),
            objects=agg.count() by accountId, changeType;
            
// 전체 보고서를 Python 룰로 전달할 수 있는 JSON 객체의 단일 행으로 만들고자 합니다
monthly_report
| project report=object('accountId', accountId, 
                        'changeType', changeType, 
                        'resourceTypes', resourceTypes,
                        'objects', objects)
| summarize monthly_report=agg.make_set(report)
```

{% endtab %}
{% endtabs %}

예시 출력:

```javascript
{
    "monthly_report": [
        {
            "ACCOUNTID": "34924069XXXX",
            "CHANGETYPE": "DELETED",
            "OBJECTS": 8,
            "RESOURCETYPES": [
                "AWS.IAM.Role"
            ]
        },
        {
            "ACCOUNTID": "34924069XXXX",
            "CHANGETYPE": "MODIFIED",
            "OBJECTS": 2388,
            "RESOURCETYPES": [
                "AWS.CloudTrail.Meta",
                "AWS.S3.Bucket",
                "AWS.CloudFormation.Stack",
                "AWS.CloudTrail",
                "AWS.IAM.Role"
            ]
        },
        {
            "ACCOUNTID": "34924069XXXX",
            "CHANGETYPE": "CREATED",
            "OBJECTS": 11,
            "RESOURCETYPES": [
                "AWS.IAM.Role",
                "AWS.CloudFormation.Stack",
                "AWS.KMS.Key"
            ]
        }
    ]
}
```

다음 `resource_history` 이 테이블은 특정 리소스 수준까지 세부 정보를 제공하므로, 필요하다면 위 검색의 더 상세한 변형을 사용할 수 있습니다.

### 데이터베이스(Snowflake) 모니터링

{% hint style="info" %}
다음과 같이 Snowflake 활동을 모니터링하는 것도 가능합니다. [Snowflake 감사 로그](https://docs.panther.com/ko/data-onboarding/supported-logs/snowflake) 통합합니다.
{% endhint %}

민감한 데이터를 보관하는 데이터베이스는 공격 대상이 되는 경우가 많으므로 광범위한 보안 모니터링이 필요합니다.

이 쿼리는 Panther의 읽기 전용 역할이 다음에 대한 접근 권한을 가지고 있어야 합니다. `snowflake.account_usage` 감사 데이터베이스(이는 Snowflake 관리자에 의해 수행되어야 할 수 있습니다).

```sql
 USE ROLE accountadmin;
 GRANT IMPORTED PRIVILEGES ON DATABASE snowflake TO ROLE panther_readonly_role;
```

이 쿼리는 사용자 이름별 실패한 로그인 패턴을 찾으며 정기적으로 실행해야 합니다:

{% tabs %}
{% tab title="SQL" %}

```sql
 -- 이전 24시간 동안 로그인 실패가 2회 초과인 사용자 반환
  -- SnowAlert 쿼리를 기반으로 수정함
  SELECT 
    user_name,
    reported_client_type,
    ARRAY_AGG(DISTINCT error_code),
    ARRAY_AGG(DISTINCT error_message),
    COUNT(event_id) AS counts
  FROM snowflake.account_usage.login_history
  WHERE 1=1
    AND DATEDIFF(HOUR, event_timestamp, CURRENT_TIMESTAMP) < 24
    AND error_code IS NOT NULL
  GROUP BY reported_client_type, user_name
  HAVING counts >=3;
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
snowflake.account_usage.login_history
| where time.diff('h', p_event_time, time.now()) < 24 and error_code != null
| summarize error_codes=agg.make_set(error_code),
            error_msgs=agg.make_set(error_message),
            counts=agg.count() by reported_client_type, user_name
| where counts >= 3
```

{% endtab %}
{% endtabs %}

단일 IP별 Snowflake 실패 로그인은 24시간 동안 IP별 로그인 시도를 살펴보고 실패한 로그인이 2회 초과인 IP를 반환합니다. 이는 잠재적으로 의심스러운 활동을 강조하기 위해 24시간마다 실행되도록 예약할 수 있습니다. 이 접근 방식의 효과는 기업이 내부 IP 주소를 처리하는 방식에 따라 달라질 수 있습니다.

{% tabs %}
{% tab title="SQL" %}

```sql
 -- 이전 24시간 동안 로그인 실패가 2회 초과인 IP 반환
  -- SnowAlert 쿼리를 기반으로 수정함
  SELECT 
    client_ip,
    MIN(event_timestamp) event_start,
    MAX(event_timestamp) event_end,
    timediff(second, event_start, event_end) as event_duration,
    reported_client_type,
    ARRAY_AGG(DISTINCT error_code),
    ARRAY_AGG(DISTINCT error_message),
    COUNT(event_id) AS counts
  FROM snowflake.account_usage.login_history
  WHERE 1=1
    AND DATEDIFF(HOUR,  event_timestamp, CURRENT_TIMESTAMP) < 24
    AND error_code IS NOT NULL
  GROUP BY client_ip, reported_client_type
  HAVING counts >= 3;
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
snowflake.account_usage.login_history
| where time.diff('h', event_timestamp, time.now()) < 24 and error_code != null
| summarize error_codes=agg.make_set(error_code),
            error_msgs=agg.make_set(error_message),
            event_start=agg.min(p_event_time),
            event_end=agg.max(p_event_time),
            counts=agg.count() by client_ip, reported_client_type
| extend event_duration=time.trunc('s', event_end) - time.trunc('s', event_start)
| where counts >= 3
```

{% endtab %}
{% endtabs %}

Snowflake의 관리자 권한 부여, 7일 전까지 되돌아봅니다. 이는 반드시 의심스러운 것은 아니지만, Snowflake 관리자가 추적해 두고 싶어할 수 있는 항목입니다.

{% tabs %}
{% tab title="SQL" %}

```sql
SELECT
    current_account() AS environment
     , REGEXP_SUBSTR(query_text, '\\s([^\\s]+)\\s+to\\s',1,1,'ie') AS role_granted
     , start_time AS event_time
     , query_text AS event_data
     , user_name AS user_name
     , role_name
     , query_type
     , query_id AS query_id
FROM snowflake.account_usage.query_history
WHERE 1=1
  AND DATEDIFF(DAY,  start_time, CURRENT_TIMESTAMP) <= 7
  AND query_type='GRANT'
  AND execution_status='SUCCESS'
  AND (role_granted ILIKE '%securityadmin%'  OR role_granted ILIKE '%accountadmin%' OR role_granted ILIKE '%admin%')
```

{% endtab %}

{% tab title="PantherFlow" %}
{% hint style="warning" %}
현재는 다음을 생성할 수 없습니다. [예약 검색](https://docs.panther.com/ko/search/scheduled-searches) 의 [PantherFlow](https://docs.panther.com/ko/pantherflow). 다음을 참조하세요. [PantherFlow의 제한 사항](https://docs.panther.com/ko/pantherflow#limitations-of-pantherflow) 자세히 알아보세요.
{% endhint %}

```kusto
snowflake.account_usage.query_history
| where time.diff('d', event_timestamp, time.now()) < 7
    and query_type == 'GRANT'
    and execution_status == 'SUCCESS'
    and (strings.ilike(role_granted, '%securityadmin%') OR 
         strings.ilike(role_granted, '%accountadmin%') OR 
         strings.ilike(role_granted, '%admin%'))
| project event_time=start_time,
          event_data=query_text,
          user_name,
          role_name,
          query_type,
          query_id,
          role_granted=re.substr(query_text, '\\s([^\\s]+)\\s+to\\s',1,1,'ie')
```

{% endtab %}
{% endtabs %}

#### 계정 사용 보기 쿼리

Snowflake 문서에서 추가 [계정 사용 조회 예시를 확인하세요](https://docs.snowflake.com/en/sql-reference/account-usage#querying-the-account-usage-views).
