룰이 900을 1000보다 크다고 판정했다 — 문자열로 비교되던 숫자 룰

에이전트 룰 엔진의 크기 비교가 숫자가 아니라 글자순으로 이뤄지고 있었다. 자릿수가 다르면 판정이 뒤집혀, 크기 비교를 쓰는 룰 7건 중 5건이 틀린 판정을 냈다. ETW 값이 전부 문자열로 들어오는 지점부터 비교기·시간차 계산까지 원인 세 곳을 짚고, 룰 파일을 한 글자도 고치지 않고 엔진에서 흡수한 기록.

개요

룰의 크기 비교가 숫자가 아니라 글자순으로 비교되고 있었다. 자릿수가 다르면 판정이 뒤집힌다.

"900" > "1000"   ->  참    ('9' > '1')

크기 비교를 쓰는 룰 7건 중 5건이 틀린 판정을 내리고 있었다. 디스크 지연·부팅 지연·시간 변경 증상이 모두 여기에 해당한다. 시계 오차 증상을 설계하다 발견했다.

노린 것

버그를 없애는 데서 끝내지 않았다. 룰 작성자가 숫자를 따옴표로 적어도 숫자로 비교되게 했다.

종전에는 {"$gte": "3600"} 과 {"$gte": 3600} 이 다르게 동작했다. 룰을 쓰는 사람이 JSON 타입까지 신경 써야 했다. 사람이 기억할 일이 아니라 엔진이 흡수할 일이다. 실제로 룰 파일이 두 방식을 섞어 쓰고 있었고 그래서 틀렸다. 이제 어느 쪽으로 적어도 같게 동작한다. 룰 파일은 한 글자도 고치지 않았다.

원인 셋

1. ETW EventData가 전부 문자열로 들어온다

XML을 JSON으로 바꾸는 곳에 숫자 판별이 없다. (agent/yagent21/CETW.cpp:2342)

parent = node->value();     //  const char*  ->  JSON 문자열

LatencyMs도 DegradationTime도 값은 숫자로 보이지만 JSON 타입은 문자열이다.

2. 비교기가 양쪽 다 JSON 숫자일 때만 숫자로 봤다

agent/module/CRule.h:1270

int ValueCompare(const Json::Value& L, const Json::Value& R){
    const bool numL = IsNumeric(L), numR = IsNumeric(R);
    if (numL && numR){ ... 숫자 비교 ... }
    ...
    int ci = _stricmp(a.c_str(), b.c_str());   //  <- 여기로 떨어졌다
}

IsNumeric은 JSON 타입만 본다. 문자열 "3600"은 숫자가 아니다. ResolveRhsToken도 {...} 형태만 해석하고 나머지는 그대로 돌려주므로, 룰에 적은 "3600"은 끝까지 문자열이었다.

3. CalcTimeDiff가 문자열을 돌려줬다

초 단위 차이를 계산하는 함수인데 agent/module/CRule.h:1412가 std::to_string 결과를 담았다. 좌변까지 문자열이 됐다. 계산 함수가 문자열을 내놓는 것 자체가 맞지 않다.

깨져 있던 곳 — 크기 비교 7건 중 5건

룰좌변우변
0011/ntfs.jsonLatencyMs 문자열1000 숫자
0011/io_latency.jsonLatencyMs 문자열"1000" 문자열
0011/boot_delay.jsonDegradationTime 문자열"5000" 문자열
0011/change_system_time.json (규칙 2개)CalcTimeDiff(...) 문자열"3600" 문자열

우변에 따옴표를 쳤는지는 상관없었다. 좌변이 문자열이라 어느 쪽이든 글자 비교로 갔다.

10분 변경(600초)      ->  「1시간 이상」에 잘못 매칭
3시간 변경(10000초)   ->  누락
0.9초 지연(900ms)     ->  「1초 이상」에 잘못 매칭

멀쩡했던 2건은 0008/DNS.json과 0008/TCP.json이다. 좌변을 에이전트가 Json::UInt64로 직접 넣어 양쪽 다 숫자였다.

고친 것 — module/CRule.h 한 파일

항목내용
#include <charconv>표준 문자열→숫자
RULE_NUMBER / AsNumber() / NumberCompare()신설. 파싱은 std::from_chars가 한다
ValueCompareAsNumber를 쓴다. 양쪽이 숫자로 읽히면 숫자로
CalcTimeDiffJson::Int64로 담는다

지켜야 할 세 가지를 표준이 그대로 만족한다.

  • 16진 거부 — base 10 고정이라 "0x10"은 'x'에서 멈춘다. 업데이트 오류 코드 14건이 종전대로 동작한다.
  • 전체 소비 — ptr != last면 덜 읽은 것이다. "1.3.15"는 두 번째 '.'에서 걸린다.
  • 범위 초과 — errc::result_out_of_range면 글자로 간다. 소수점이나 지수가 없으면 long long으로 읽는다. double로 통일하면 2^53을 넘는 주소·핸들이 뭉개진다.

빈 문자열과 앞 공백도 표준이 거부한다. 그래서 ""는 0과 같아지지 않는다.

$eq도 같이 바뀐다

ValueEquals가 ValueCompare를 그대로 쓴다. 갈라놓지 않았다. 갈라두면 $gte "141"은 숫자로, $eq "141"은 글자로 비교되어 같은 룰 파일 안에서 규칙이 어긋난다.

같음 비교 681건은 원래 정상이었다. 글자로 떨어질 때 ToString으로 양쪽을 정규화하므로 결과가 맞았다. 이 변경으로 달라지는 것은 선행 0 다섯 건뿐이다. "00000008"이 "8"과 같아지는데, EXCEPTION_ACCESS_VIOLATION의 예외 파라미터(읽기 0 / 쓰기 1 / DEP 8)라 숫자로 보는 편이 옳다.

$regex는 ValueCompare를 타지 않아 영향이 없다.

검증

orange.exe --rule로 12건을 확인했다. 전부 통과.

  • 고쳐진 것 4건 — 시간 변경 600초·10000초, 디스크 지연 900ms·1500ms
  • 지켜진 것 8건 — 16진·버전 문자열·빈 문자열·앞 공백·선행 0·쉼표·정수 크기

Orange Platform 에이전트 룰 엔진의 숫자 비교 정정에 관한 기술 리포트입니다.