목차/12. Syslog 수동 파싱

12Syslog 수동 파싱Syslog 수동 파싱

수많은 타사 장비가 로그를 남길 때 syslog 형식을 씁니다. Check Point의 Log Server 는 이 원시(raw) syslog 데이터를 Check Point 로그 형식으로 다시 짜 맞춰 처리하고, 그렇게 정리된 로그를 SmartEvent 가 받아 보안 이벤트(security event)로 변환합니다. 결국 타사 장비의 로그가 Check Point 환경 안에서 1급 시민처럼 다뤄지려면, "이 장비가 보낸 한 줄짜리 텍스트 로그에서 무엇을 어떻게 뽑아낼지"를 정의하는 파싱 파일(parsing file) 이 있어야 합니다.

파싱 파일을 만드는 길은 두 가지입니다. 하나는 GUI 도구인 Log Parsing Editor 로 만드는 것이고(타사 로그 가져오기의 "Importing Syslog Messages" 참고), 다른 하나는 파싱 파일을 손으로 직접 작성 하는 것입니다. 이 장은 후자 — 수동 파싱 — 을 위한 모든 것을, 즉 사전 준비부터 Log Server에서 파싱이 일어나는 절차, 파일을 만들고 검증·문제 해결하는 흐름, 그리고 파싱 언어(Free Text Parsing Language)의 명령·필드·dictionary 문법까지 빠짐없이 정리합니다.

시작 전에 — 계획과 고려사항 (Planning and Considerations)

좋은 파서를 짜는 일의 절반은 코드를 쓰기 전 준비에서 결판납니다. 핵심은 장비가 만들어 내는 로그의 구조를 정확히 아는 것 입니다. 순서대로 다음을 챙깁니다.

  1. 장비가 생성하는 로그의 구조를 정확히 파악합니다. 이를 위해 두 종류의 자료를 모읍니다.
    • 벤더의 로깅 가이드(또는 그 장비가 만들 수 있는 로그의 종류와 구조를 명시한 문서). 문서가 중요한 이유는 가능한 모든 로그 종류를 빠짐없이 찾았는지 확인 하기 위해서입니다. 보통은 이 문서만으로도 파싱 파일을 쓰기에 충분합니다.
    • 로그 샘플 — 가능한 한 많이. 반드시 SmartEvent에 실제로 연동할 그 장비에서 직접 뽑은 로그 를 쓰세요. 샘플은 만든 파싱 파일을 검사하고 그에 맞춰 튜닝 하는 데 쓰입니다.
  2. Free Text Parsing 언어를 익히고, 필요한 파싱 파일들과 그것들이 Log Server 어디에 있는지(아래 "Log Server에서 파싱이 일어나는 절차" 참고)를 알아 둡니다.
  3. 비슷한 부류의 제품을 위한 기존 파싱 파일과 견주어 봅니다. 처음부터 짜기보다 닮은 것을 본떠 시작하는 편이 빠르고 안전합니다.
  4. 로그에서 뽑아낼 필드를 고릅니다. 뽑을 필드는 장비마다 다르지만, 같은 부류(category)의 장비는 보통 비슷한 로그 필드를 가집니다. 예를 들면 다음과 같습니다.
장비 유형(Device Type)흔히 갖는 로그 필드(Typical Log Fields)
방화벽, 라우터 등 연결 기반(connection based) 로그 를 보내는 장비출발지 IP 주소, 목적지 IP 주소, 출발지 포트, 목적지 포트, 프로토콜, accept/reject 표시
IDS / IPS, 애플리케이션 방화벽 등 공격(attack) 로그 를 보내는 장비공격 이름(attack name) / ID

Log Server에서 파싱이 일어나는 절차 (The Parsing Procedure)

파싱은 Log Server에서, syslog 데몬으로부터 시작 됩니다. Log Server에서 도는 syslog 데몬이 syslog를 받아 그 파싱을 호출합니다. 파싱에는 여러 파싱 파일이 관여하는데, 이들은 서로 다른 파싱 정의·명세를 담고 있으며 모두 $FWDIR/conf/syslog/ 디렉터리에 있습니다. 이 디렉터리 안에는 장비별(device-specific) 파싱 파일 들이 있어, 각 장비 고유의 syslog 형식에 맞춰 실제 파싱과 필드 추출을 정의합니다.

파싱의 출발점은 syslog_free_text_parser.C 파일입니다. 이 파일은 여러 "Dictionary" 용어(아래 참고)를 정의하면서 syslog를 파싱하는데, 모든 syslog 메시지에 공통인 필드(PRI, 날짜와 시각 등)와 그 syslog를 만든 머신·애플리케이션 을 먼저 뽑아냅니다.

그다음 syslog_free_text_parser.CallDevices.C 파일을 사용합니다. allDevices.C 는 다시 두 파일을 참조합니다.

  • UserDefined/UserDefinedSyslogDevices.C사용자가 정의한 장치 파싱 파일들의 이름이 담긴 파일.
  • CPdefined/CPdefinedSyslogDevices.CCheck Point가 정의한 장치 파싱 파일들이 담긴 파일.

allDevices.C 는 이 장치 파싱 파일들을 차례로 훑으면서, 들어온 syslog를 각 파일이 파싱하는 syslog 형식과 맞춰 봅니다. 어떤 파싱 파일이 예비 파싱(preliminary parsing)에 성공 하면 — 즉 그 syslog 형식과 일치하여 그 syslog의 출처(origin)로 판명되면 — syslog의 나머지도 그 파일이 마저 파싱합니다. 매칭되는 파일을 못 찾으면, Check Point 장치 파싱 파일들로 계속 넘어가 일치하는 것을 찾을 때까지 시도합니다.

수동 파싱 파일 만들기 (Manual Syslog Parsing)

syslog 파일을 파싱하도록 직접 설정하는 흐름은 다음과 같습니다.

  1. <장비 제품명>.C 라는 새 파싱 파일을 만듭니다. (예: snortPolicy.C)
  2. 이 파일을 Log Server의 $FWDIR/conf/syslog/UserDefined 디렉터리에 둡니다.
  3. Log Server에서 $FWDIR/conf/syslog/UserDefined/UserDefinedSyslogDevices.C 파일을 편집해, 새 파싱 파일을 include 하는 줄 을 추가합니다. 예를 들면 이렇게 씁니다.
   : (
               :command (
                     :cmd_name (include)
                     :file_name ("snortPolicy.C")
               )
         )
   
  1. (선택) 필요하다면 dictionary 파일을 함께 만듭니다.
    • <장비 제품명>_dict.ini 라는 새 dictionary 파일을 만듭니다(아래 "Dictionary" 참고).
    • 이 파일도 Log Server의 $FWDIR/conf/syslog/UserDefined 디렉터리에 둡니다.
    • dictionary는 서로 다른 장비의 로그에서 같은 의미를 가진 값들을 하나의 공통 값으로 번역 하며, 이 공통 값이 Event Definition 에서 쓰입니다.
    • Log Server에서 $FWDIR/conf/syslog/UserDefined/UserDefinedSyslogDictionaries.C 파일을 편집합니다.
    • dictionary 파일을 include 하는 줄을 추가합니다. 예:
     :filename ("snort_dict.ini")
     
  1. 파서를 검사하려면 Check Point Log Server로 syslog 샘플을 보냅니다. (아래 절차)

syslog 샘플 보내서 검증하기

만든 파서가 의도대로 도는지 보려면, 실제 syslog 샘플을 Log Server로 흘려보내 확인합니다.

  1. Log Server가 syslog를 받아들이도록 설정하기 위해, SmartConsole 로 Security Management Server에 연결합니다.
  2. Logs and Masters > Additional Logging Configuration 에서 Accept Syslog messages 속성을 켭니다.
  3. Log Server 네트워크 객체를 편집합니다.
  4. 다음 명령을 실행합니다.
   cpstop & cpstart
   

또는

   fw kill fwd & fwd -n
   

이때 Log Server의 fwd 프로세스가 재시작 됩니다. 5. 장비 자체에서, 또는 syslog 생성기(generator)에서 syslog를 보냅니다. 예를 들어 Kiwi Syslog Message Generator (http://www.kiwisyslog.com/software_downloads.htm#sysloggen)를 쓸 수 있습니다.

문제 해결 (Troubleshooting)

SmartConsole에 로그가 기대대로 나오지 않으면, 파싱 파일에 문제가 있는 것입니다. 증상별로 다음을 점검합니다.

  • 구문 오류(syntax error)가 있으면 오류 메시지가 표시됩니다. 특정 오류 메시지를 자세히 읽으려면, fwd -n 절차를 실행하기 전에 TDERROR_ALL_FTPARSER 값을 5로 설정하세요.
  • SmartConsole에 syslog가 'Product syslog' 로 표시되면, 그 로그는 제대로 파싱되지 못하고 일반 syslog로 처리 된 것입니다.
  • Product 필드에 (방금 추가한 게 아니라) 엉뚱한 다른 제품이 들어 있으면, 그 다른 제품의 파싱 파일에 문제 가 있다는 뜻입니다. 이 경우 Check Point SmartEvent 팀에 보고하세요.
  • 제품이 로그에 제대로 보고되면, 뽑아낸 모든 필드를 찾아보세요. 일부는 Information 섹션 에 있고, 일부 필드는 More Columns 를 선택해야만 보입니다.

Free Text Parsing 언어 (The Free Text Parsing Language)

Free Text Parsing 언어는 입력 문자열을 파싱하고, 정보를 뽑아내며, 로그 필드를 정의 하는 일을 합니다. 이렇게 정의된 로그 필드는 Log Server에서 Check Point 로그의 일부로 나타나며, 이벤트(event)를 정의하는 데 쓰입니다.

각 파싱 파일은 명령(command)들의 트리(tree) 로 이뤄집니다. 명령은 저마다 입력 문자열의 일부를 검사하거나 파싱하고(때로는 그 결과로 로그에 필드를 더하고), 자신의 실행이 성공했는지 실패했는지에 따라 문자열을 계속 파싱할지를 결정 합니다.

명령의 구성 (The Commands)

명령 하나는 다음 부분들로 이뤄집니다.

  • cmd_name — 명령의 이름.
  • command arguments — 명령의 동작을 정의하는 인자들.
  • on_success(선택) — 현재 명령의 실행이 성공하면 다음에 실행할 명령.
  • on_fail(선택) — 현재 명령의 실행이 실패하면 다음에 실행할 명령.

기본 골격은 다음과 같습니다.

:command (
   :cmd_name (try)
   :try_arguments
        .
        .
   :on_success (
       :command()
   )
   :on_fail (
       :command()
   )
)

on_success/on_fail 가지치기가 곧 "명령의 트리"를 이룹니다. 아래에서 실제 명령들을 하나씩 봅니다.

Try

try 명령은 정규식(regular expression)을 입력 문자열과 매칭 합니다.

인자(Argument)설명
parse_from정규식을 입력의 어디부터 돌릴지 정합니다. start_position입력 문자열의 처음 부터 정규식을 실행합니다. last_position직전에 성공한 명령이 멈춘 위치 부터 실행합니다.
regexp매칭할 정규식.
add_field(정규식이 성공했을 때만) 결과에 더할 하나 이상의 필드.

예제입니다.

:command (
     :cmd_name (try)
     :parse_from (start_position)
     :regexp ("([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)")
     :add_field (
       :type (index)
       :field_name (Src)
       :field_type (ipaddr)
       :field_index (1)
     )
)

위 예제는 "([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)" 라는 정규식을 로그 전체를 대상으로(parse_from (start_position), 로그 처음부터) 매칭하려 시도합니다. 정규식이 매칭되면 출발지(Src) 필드를 하나 더합니다.

Group_try

group_try 명령은 하나 이상의 명령을 다음 모드 중 하나로 실행 합니다.

  • try_all — 그룹 안의 모든 명령을 시도하며, 각 명령의 반환 코드는 무시 합니다.
  • try_all_successively — 그룹 안의 모든 명령을 시도하고 반환 코드는 무시하되, 각 명령은 직전에 성공한 명령이 멈춘 위치부터 실행하려 합니다.
  • try_until_success하나가 성공할 때까지 모든 명령을 시도합니다.
  • try_until_fail하나가 실패할 때까지 모든 명령을 시도합니다.

group_try 는 흔히 뽑아내고 싶은 여러 필드가 들어 있는 로그의 "자유 텍스트(free-text)" 조각을 파싱 할 때 씁니다. 예를 들어 다음과 같은 로그가 있다고 합시다.

%PIX-6-605004: Login denied from 194.29.40.24/4813 to
outside:192.168.35.15/ssh for user 'root'

이 로그 조각을 볼 때는 다음과 같은 구조를 쓸 수 있습니다.

:command (
   :cmd_name (group_try)
   :mode (try_all_successively)
   :(
      # A "try" command for the source.
      :command ()
   )
   :(
      # A "try" command for the destination.
      :command ()
   )
   :(
      # A "try" command for the user.
      :command ()
   )
           .
           .
           .
)

이 예제에서는 group_try 블록 안의 첫 번째 try 명령(출발지용)부터 실행됩니다. 만약 출발지·목적지·사용자가 syslog 안에서 정해진 순서로 나타나지 않는다면, try_all_successively 대신 try_all 모드를 쓰세요.

또 다른 쓰임은, 서로 다른 명령의 정규식들이 더 구체적인 로그들을 매칭 하도록 두는 것입니다. 이때 group_try 블록에서 기껏해야 하나의 명령만 성공 하며, 성공하는 명령을 찾으면 나머지는 검사할 필요가 없습니다.

:command (
   :cmd_name (group_try)
   :mode (try_until_success)
   :(
      :command (
      .
      .
      .
        :regexp ("(\(|)(login|su)(\)|).* session (opened|closed) for user ([a-z,A-Z,0-9]*)")
      )
    )
   :(
      :command (
       .
       .
       .
        :regexp ("(\(|)su(\)|).* authentication failure; logname=([a-zA-Z0-9]*).* user=([a-zA-Z0-9]*)")
      )
    )
      .
      .
      .
)

Switch

switch 명령은 지정한 필드의 값을, 미리 정한 상수 값 목록과 비교 할 수 있게 해 줍니다.

파라미터(Parameter)설명
field_name값을 검사할 필드 이름.
case하나 이상의 case 속성 과, 그 뒤에 비교할 값을 둡니다.
default해당하는 case하나도 없을 때만 실행됩니다. default 값은 선택입니다.

예제입니다.

:command (
   :cmd_name (switch)
   :field_name (msgID)
   :(
      :case (302005)
      :command ()
     )
   :(
      :case (302001)
      :case (302002)
      :command ()
     )
   :default (
      :command()
     )
)

Unconditional_try

unconditional_try아무 조건 없이 결과에 필드를 더할 수 있게 하는 "빈(empty)" 명령입니다.

:command (
   :cmd_name (unconditional_try)
   :add_field (
      :type (const)
      :field_name (product)
      :field_type (string)
      :field_value ("Antivirus")
   )
)

unconditional_try 는 흔히 switch 명령과 함께 씁니다. 다음 예제는 각 메시지 ID에 그 의미를 나타내는 "message" 필드를 붙여 줍니다.

:command (
   :cmd_name (switch)
   :field_name (msgID)
(
   :case (106017)
   :command (
      :cmd_name (unconditional_try)
      :add_field (
      :type (const)
      :field_name (message)
      :field_type (string_id)
      :field_value ("LAND Attack")
      )
   )
   )
   :(
   :case (106020)
   :command (
      :cmd_name (unconditional_try)
      :add_field (
      :type (const)
      :field_name (message)
      :field_type (string_id)
      :field_value ("Teardrop Attack")
      )
   )
   )
   .
   .
   .
)

Include

include 명령은 새 파싱 파일을 끼워 넣을(include) 수 있게 합니다.

인자설명
file_nameinclude 할 파일의 전체 경로와 파일 이름.
:command (
   :cmd_name (include)
   :file_name ("c:\freeTextParser\device\antivirusPolicy.C")
)

Add_field

add_field 는 뽑아낸 정보를 실제 로그 필드로 만드는 명령으로, 다음 파라미터들을 가집니다.

typeadd_field 명령의 유형. 다음 값을 가질 수 있습니다.

  • Index — 정규식의 일부를 필드로 추출 합니다. 어느 부분을 추출할지는 field_index 값이 가리킵니다(아래 참고).
  • Const정규식에서 뽑은 정보에 의존하지 않는 상수 필드 를 더합니다. 값은 field_value 로 줍니다.

field_name — 새 필드의 이름. 그런데 일부 필드는 SmartConsole > Logs & Events > Logs대응하는 열(column) 을 가집니다. 다음 표의 이름으로 필드 이름을 지으면, (다른 추가 필드들이 들어가는 Information 필드가 아니라) 그 전용 열에 표시 됩니다.

줄 이름(Field Name to be Given)Logs & Events > Logs의 열(Column)
SrcSource
DstDestination
protoProtocol
s_portSource Port
productProduct
serviceService (resolve되면 포트와 프로토콜을 포함)
ActionAction
ifnameInterface
UserUser

위 이름들로 필드를 지으면 올바른 열에 자리 잡고, 그 열에서 이뤄지는 모든 필터링에 참여하며, 같은 이름을 쓰는 기존 이벤트 정의에도 자동으로 동참 합니다.

field_type — 로그에서 그 필드의 유형. 가능한 값은 다음과 같습니다.

필드 유형(Field Type)설명(Comment)
int / uint / string정수 / 부호 없는 정수 / 문자열.
ipaddrSrc·Dst 필드에 쓰는 IP 주소용.
prisyslog의 facility와 severity 를 포함.
timestmpsyslog의 날짜와 시각 을 포함. 'Oct 10 2019 15:05:00' 형식 지원.
time'15:05:00' 형식 지원.
string_id문자열을 더 효율적으로 다루기 위한 유형. 그 필드가 가질 수 있는 값이 유한한(finite) 개수 일 때 씁니다.
action다음 액션 지원: drop, reject, accept, encrypt, decrypt, vpnroute, keyinst, authorize, deauthorize, authcrypt, default.
ifdir0 = inbound(인바운드), 1 = outbound(아웃바운드).
ifname인터페이스 이름용(ifname 필드와 함께 사용).
protocol필드 이름은 반드시 proto 여야 합니다.
portservice·s_port·port 필드용.

그리고 다음 필드 이름들은 반드시 표에 적힌 그 필드 유형 을 써야 합니다.

필드 이름(Field Name)필드 유형(Field Type)
Srcipaddr
Dstipaddr
protoprotocol
s_portport
serviceport
Actionaction
ifnameifname

field_index 또는 field_value — 어느 쪽을 쓰는지는 type 값에 달려 있습니다.

  • typeindex 이면 field_index 가 나타납니다.
  • typeconst 이면 field_value 가 나타납니다.

field_index정규식의 어느 부분을 추출할지 를, 패턴의 그룹화(grouping)에 따라 가리킵니다. 그룹화는 정규식의 일부를 괄호 ( ) 로 묶어 만들며, field_index 의 숫자가 몇 번째 괄호의 패턴 을 가져올지를 뜻합니다.

다음은 index 타입을 세 번 쓰는 예제입니다.

:command (
   :cmd_name (try)
   :parse_from (last_position)
   :regexp ("Failed password for ([a-zA-Z0-9]+) from ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) port ([0-9]+)")
   :add_field (
      :type (index)
      :field_name (User)
      :field_type (string)
      :field_index (1)
     )
   :add_field (
      :type (index)
      :field_name (Src)
      :field_type (ipaddr)
      :field_index (2)
   )
   :add_field (
      :type (index)
      :field_name (port)
      :field_type (port)
      :field_index (3)
   )
)

여기서 User의 패턴 [a-zA-Z0-9]+첫 번째 괄호 쌍 에 있으므로 field_index 가 1입니다. 출발지 주소 패턴 [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+두 번째 괄호 에 있으니 인덱스가 2이고, 포트 패턴은 세 번째 괄호 에 있습니다.

:command (
   :cmd_name (try)
   :parse_from (start_position)
   :regexp ("access-list (.*) (permitted|denied|est-allowed) ([a-zA-Z0-9_\([a-zA-Z0-9_\\.[0-9]+\.[0-9]+\.[0-9]+)\(([0-9]*)\)
-> "))
   :add_field (
      :type (index)
      :field_name (listID)
      :field_type (string)
      :field_index (1)
   )
   :add_field (
      :type (index)
      :field_name (action)
      :field_type (action)
      :field_index (2)
   )
   :add_field (
      :type (index)
      :field_name (proto)
      :field_type (protocol)
      :field_index (3)
   )
   :add_field (
      :type (index)
      :field_name (ifname)
      :field_type (ifname)
      :field_index (4)
   )
   :add_field (
      :type (index)
      :field_name (Src)
      :field_type (ipaddr)
      :field_index (5)
   )
   :on_success (
      :command (
         :cmd_name (try)
         :parse_from (last_position)
         :regexp ("([a-zA-Z0-9_\\.[0-9]+\.[0-9]+\.[0-9]+)\(([0-9]*)\) hit-cnt ([0-9]+) ")
         :add_field (
            :type (index)
            :field_name (destination_interface)
            :field_type (string)
            :field_index (1)
         )
      )
   )
)

typeconst 일 때는 field_value더할 상수 값 이 됩니다.

:command (
   :cmd_name (try)
   :parse_from (last_position)
   :regexp ("%PIX-([0-9])-([0-9]*)"))
   :add_field (
       :type (const)
       :field_name (product)
       :field_type (string_id)
       :field_value ("CISCO PIX")
   )
)

dict_name — 값을 변환할 때 사용할 dictionary의 이름 입니다. 만약 그 값이 dictionary에서 발견되지 않으면, 원래 값이 그대로 결과 가 됩니다. dictionary를 써서 값을 변환하는 예제입니다.

:command (
   :cmd_name (try)
   :parse_from (start_position)
   :regexp ("list (.*) (permitted|denied) (icmp) ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) -> ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*
packet")
   :add_field (
         :type (index)
         :field_name (action)
         :field_type (action)
         :field_index (2)
         :dict_name (cisco_action)
    )
)

Dictionary

Free text parser는 dictionary를 써서 로그의 값을 변환 할 수 있습니다. 이 변환의 목적은 서로 다른 장비의 로그에서 같은 의미를 가진 값들을, 하나의 공통 값으로 번역 하는 것입니다. 그 공통 값이 이벤트 정의(event definition)에서 쓰입니다. 예컨대 Cisco 장비는 permitted/denied 라 쓰고 3Com 장비는 Permit/Deny 라 쓰더라도, dictionary를 거치면 둘 다 accept/reject 라는 공통 값으로 통일됩니다.

각 dictionary 파일은 .ini 파일 로 정의됩니다. .ini 파일에서 섹션 이름(section name)이 곧 dictionary 이름 이고, 그 아래의 값들이 dictionary 값입니다. 하나의 dictionary 파일은 하나 이상의 섹션 을 담을 수 있습니다.

Name1 = val1
Name2 = val2
[cisco_action]          [3com_action]
permitted = accept      Permit    = accept
denied = reject         Deny      = reject

파싱 파일에서 dictionary를 참조하는 모습은 다음과 같습니다 — add_field 안에서 dict_name 으로 어떤 dictionary를 쓸지 가리킵니다.

:command (
      :cmd_name (try)
      :parse_from (start_position)
      :regexp ("list (.*) (permitted|denied) (icmp) ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) -> ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*
packet")
       :add_field (
               :type (index)
               :field_name (action)
               :field_type (action)
               :field_index (2)
               :dict_name (cisco_action)
)
)

이 예제는 정규식의 두 번째 괄호(permitted|denied)에서 뽑은 값을, cisco_action dictionary를 거쳐 변환한 뒤 action 필드에 넣습니다. 결과적으로 permittedaccept 로, deniedreject 로 바뀌어 Check Point 표준 액션 값 으로 로그에 남고, 이 표준 값을 기준으로 SmartEvent의 이벤트 정의가 일관되게 동작합니다.

GUI로 더 쉽게 파서를 만드는 길은 타사 로그 가져오기의 Log Parsing Editor를 참고하세요.