sprocess 1시간 hourly 집계 + Rotate 공통 진입점, MQTT subscriber-driven publish로 31k SUID 폭증 차단, Manager Web 디자인 시스템 정립 — 주간 리뷰 (2026-06-01 ~ 2026-06-08)

매니저·에이전트 파트가 sprocess 1시간 단위 집계 체계를 Rotate 공통 진입점으로 완성하고 stall 회귀를 #if 0 격리로 잡았으며, 서버 백엔드 파트가 프로세스 영향도 위젯 서버를 4-레포 동시 정리하고 subscriber-driven publish로 31k SUID 시간당 224k msg 폭증을 차단, 분석 서비스 파트가 Baseline 일상 분류 서비스 Sprint 2(Override CRUD + verdict_history)와 위젯 ACL 빈 배열 시맨틱 통일로 보안 사고 경로를 막고, 커널 드라이버 파트가 orange.sys Y_PROCESS_MESSAGE Integrity Level end-to-end 파이프라인과 MS 서명 자동화 빌드 서버 내장 비동기 Job 패턴을 깐 한 주. 프론트엔드 파트가 Manager Web 디자인 시스템(DESIGN_SYSTEM.md)을 정립.

2026년 6월 1주차(2026-06-01 ~ 2026-06-08, ISO week 24) 주간 리뷰. 한 주 동안 푼 문제 중심으로 정리한다.

룰·데이터 분석 파트는 산출물이 코드가 아니라 GitHub 카운트로 잡히지 않으므로, 아래 별도 섹션에서 매니저 위젯 포팅과 분석 결정을 정리한다.


Agent sprocess 1시간 hourly 집계 체계 완성

sprocess 1시간 단위 hourly 집계 체계 완성 + stall 회귀 격리 (OR-1340). OR-1306이 깐 1시간 주기 송신 인프라 위에서 송신 수치의 의미를 "부팅 후 누적"에서 "1시간 단위 집계"로 교체했다. 핵심은 CSProcessHourly 클래스 하나에 SUID 기반 1시간 버킷 메모리 집계·Rotate 공통 진입점·Cleanup·FlushAll·DebugFlush(dev 모드)를 모두 모으고, 절전·시계 변경·VM 재기동처럼 시간이 갑자기 건너뛰는 모든 상황을 Rotate 헬퍼가 흡수하도록 통합한 것이다. 모든 진입점이 같은 함수를 거치니 시간 점프 처리 누락이 원천 차단된다.

데이터 모델도 같이 정리했다. sprocess는 부팅 누적(실시간), sprocess_hourly는 시간 집계로 양 테이블 역할을 분리하고 서버 전송은 hourly만으로 확정. Running은 저장 컬럼을 모두 제거하고 8개 SELECT에서 MAX(Start − Stop, 0) AS Running 파생으로 전환해 0 복귀가 자동으로 따라온다. sprocess_hourly의 Running은 MAX(Start2 + Start − Stop, 0). Rotate 시간대 전환 시 new.start2 = MAX(old(start2+start-stop), 0)로 시작값을 자동으로 넘기니, 한 시간대의 마지막 Running이 다음 시간대의 시작값이다. 28시간 무중단 운영에서 40회 연속 시간대 전환 carry mismatch 0건.

이번 주의 가장 어려웠던 부분은 stall 회귀 진단이다. sprocess 실시간 누적을 위해 5개 증상 카운터(Kill/Killed/Crash/Crashed/Detect)에 신규 SPROCESS_UPDATE_* 쿼리 4개를 추가한 직후, summary.cdb 메인 파일 쓰기가 1시간 동안 멈추는 회귀가 나타났다. Agent 자체는 정상 동작하지만 외부 reader(orangedb.exe)에서 라이브 데이터가 보이지 않는다. 4개 신규 콜 사이트를 #if 0으로 임시 격리하고 28시간 검증한 결과 stall이 사라져, lock contention 가설을 확정했다. wal_autocheckpoint = 1 노브 신설로 매 commit마다 WAL → 메인 파일이 즉시 flush되도록 db/summary.json 헤더에 박았다. dev 모드(C:@DEV)에선 transaction=0 + wal_autocheckpoint=1을 강제로 적용해 매 이벤트마다 디스크 반영이 즉시 보이게 만들었다. SystemStat MQTT 송신 빈도 폭증(평상시 60초 / LiveResponse 5초)도 같은 작업에서 정리. SUID_Counter의 잔존 detected/changed 필드는 익명 nested struct var{}로 묶어 의미 분리(카운터 vs slist 임시 단발 플래그). 이번 주 5커밋, 진행 중. 안승민과 송신 메시지 스키마 합의·Safe 모드 정책만 남은 단계.

Agent Server 접속 시나리오 통합 + 외부 단말 자가 복구 경로 복원 (OR-1449). 한 단말이 매 부팅마다 사내망 사설 IP(192.168.100.211:3181) 하나에만 등록 시도하다 매번 실패하는 상태가 자가 복구되지 않던 사고를 진단·해결. 본사 서버 배포 도중 응답의 doc.Servers가 잠깐 1개(사내망)로 줄어든 순간을 받은 외부 단말이, 받은 1개로만 RegisterToServer2를 시도하다 응답이 안 오니 새 서버 목록도 못 받고, OR-1274에서 자동 시도 회복을 일부러 끊으면서 단말 스스로 고칠 길도 같이 막혀 있던 케이스다. 사내 단말은 사설 IP에 닿을 수 있어 문제가 안 드러나고, 연구소나 외부망 단말만 영원히 닿지 못한다.

접속 우선순위를 Override Server → Servers → LastServer → DefaultServer로 명문화하고 LastServer 개념을 추가했다. LastServer는 그 단말이 한 번이라도 등록 성공한 서버다. 한 번이라도 성공 이력이 있는 단말은 다음 부팅부터 LastServer로 돌아가 자기 자리를 다시 찾는다. LastServer가 채워져 있으면 DefaultServer는 시도 후보에서 영원히 빠진다 — 고객사 단말이 우리 회사 기본 매니저로 가버리는 사고를 동시에 막는 정책이다. UI 저장 분기의 "기존값과 같으면 무시"(command.cpp:262) 가드도 제거 — 사용자가 같은 값을 다시 저장해도 무조건 SetServerOverride를 호출하므로 다음 등록 사이클에서 그 주소로 다시 시도되고, 서버가 정상 응답으로 새 서버 목록을 내려주면서 단말이 자연스럽게 정상화된다.

변경 이력 추적을 위해 server.log 폭주 정리도 같이 했다. module/logger.h에 dedup_sink를 추가 — 같은 줄이 계속 들어오면 메모리에서 횟수만 세고, 다른 줄이 들어오는 순간 [REPEAT N] 한 줄을 자동 출력한다. 호출하는 쪽 코드는 손대지 않는다. RegisterToServer2 출력을 한 사이클당 한 줄로 정리한 뒤, 35회 반복이 2줄로 압축되는 것을 빌드 검증으로 확인. 4MB 한도는 그대로 두되 폭주가 사라져 한도에 닿는 시간이 훨씬 길어졌다. 다음 단계는 단말마다 서버 주소가 언제 어떻게 바뀌었는지의 영구 기록(node_server_history 컬렉션 + MQTT 전송)과 매니저에서 단말 서버 주소를 원격으로 정정하는 명령. 이번 주 1커밋, 완료.


REST API · MQTT · MongoDB 다섯 축 동시 정리

프로세스 영향도 위젯 — 서버 구현 4-레포 동시 정리 + subscriber-driven publish (OR-1442). OR-1334 분석가 가이드를 서버 입장에서 검토·확장해 rest-api · service · migrations · package 네 서브모듈에 걸쳐 동시 정리. 지난 주에 굵직한 설계 결정을 끝낸 뒤 이번 주에는 e2e 통합과 후속 follow-up을 마무리해 In QA로 진입했다.

핵심은 subscriber-driven publish 모델이다. 개발 서버 기준 sprocess 컬렉션 SUID는 31,117개. 이 규모를 stateless로 전수 publish하면 시간당 ~224k msg가 되는데, 활성 매니저가 보는 SUID는 1개 ~ 수십 개이므로 99% 이상이 아무도 안 듣는 publish다. manager-web이 관심 SUID를 PUT /v3/sprocess/subscribe로 등록하면 Redis ZSET에 ZADD(score=expire epoch sec)로 추적하고, aggregator는 publish 직전 ZRANGEBYSCORE sprocess:subscribers:{bucket} now +inf로 활성 SUID set을 조회해 집계 결과와 교집합만 publish한다. body.suids는 그 시점 manager의 관심 SUID set(전체 snapshot)이다. keepalive: manager-web이 약 3분마다 같은 PUT을 재호출 → score 갱신. 다수 manager가 같은 SUID를 공유하면 각자 keepalive가 독립 적용되어, 어느 한 명의 위젯 종료가 다른 manager의 publish에 영향을 주지 않는다. 명시적 unsubscribe는 없고 ZADD-only 시맨틱 — score < now로 자연 만료될 때만 사라진다. cleanup은 PUT 호출마다 ZREMRANGEBYSCORE -inf now를 L-pipeline 1 RTT로 동반. abuse 가드: ZADD 후 ZCARD 검사로 한 bucket의 ZSET 멤버 수가 MAX_ZSET_SIZE = 100,000을 넘으면 방금 추가한 SUID들을 ZREM으로 롤백 + 429. 정상 운영(매니저 수 × 위젯 × SUID 수십개 ≈ ~100)이 cap의 1000배 여유 안에 있어 일반 사용엔 절대 닿지 않는다.

이번 주 마무리 작업은 aggregated/list의 SUID 메타 8 필드 합류(B-7)다. 분석가 follow-up — "SUID만 보면 관리자가 어떤 실행파일인지 알기 어렵다. ProcName / ProductName / ProductVersion / CompanyName / FileName / FileVersion / FileDescription / MD5가 있으면 좋겠다". 이 8 필드는 SUID 단위로 고정(PRunUID+FastHash+RunUID hash이므로 한 SUID = 한 실행파일)이고, sprocess raw의 71.4%가 ProductName 보유, 8.8%는 모든 doc에서 메타 0%, 19.6%는 doc별 혼재. GetSprocessAggregatedListCase의 pipeline 끝에 $skip/$limit 뒤에서 $lookup sprocess로 채택 — sprocess_hourly schema 무변경, aggregator 무변경, 변경 폭 최소. 같은 SUID의 메타 보유 doc을 우선 take하기 위해 sub-pipeline $sort {ProductName: -1, _id: 1} + $limit 1로 mongo null sort 규칙을 활용한다. sprocess {SUID:1} 인덱스가 필수(기존 복합 인덱스 prefix=id라 SUID-only COLLSCAN 4.7s) → v0013 마이그레이션에 추가. 별도 systemd 서비스 orange.process-aggregator.service로 격리, hourly cron :02 UTC + partial :05/:15/:25/:35/:45/:55 UTC, daily cron 00:30 서버 로컬 + partial 06:15 / 12:15 / 18:15 서버 로컬. 위젯이 새 hour 시작 후 최대 5분 안에 진행 중 hour 데이터를 본다. 이번 주 6커밋, In QA.

MQTT broker 드롭/백프레셔 위젯 노출 + WS 모드 크래시 수정 (OR-1412). Mosquitto가 publish하는 메시지 드롭/백프레셔 관련 $SYS 토픽 4개를 수집·노출해 슬로우 컨슈머나 트래픽 burst로 인한 broker QoS 0/1/2 메시지 손실을 운영자가 실시간 감지할 수 있게 만들었다. $SYS/broker/publish/messages/dropped(누적 드롭), $SYS/broker/load/publish/dropped/1min(분당 드롭율 이동평균), $SYS/broker/store/messages/count(스토어 보관 — 적체 지표), $SYS/broker/clients/disconnected(영속 세션 보유 오프라인 클라이언트). 설계 단계에서 4번째 지표로 잡았던 인플라이트($SYS/broker/messages/inflight)는 운영/dev의 mosquitto 2.0.18에 해당 토픽이 존재하지 않아, 동일하게 백프레셔/적체를 가늠할 수 있는 스토어 보관($SYS/broker/store/messages/count)으로 대체. drop·drop율 > 0이면 text-red-500으로 강조. 매니저 위젯에서 wsConnected 식별자가 미정의되어 WS 모드(useWebSocket: true)에서 ReferenceError로 렌더가 깨지던 런타임 크래시도 같이 수정 — 헤더의 파란 "Live" 뱃지가 정상 표시되도록 했다. Playwright MCP로 dev 라이브 데이터에 OrangeServerTab을 레이어 윈도우 팝업으로 띄워 WS/폴링 두 모드 모두 검증. 이번 주 완료.

REST API 3181 nginx 우회 스캔 fail2ban HTTP jail + IP 위조 방어 (OR-1448). REST API 컨테이너 포트 3181은 Agent 직접 접속용으로 인터넷에 공개되어 있어, nginx(443)를 우회한 외부 스캐너가 GET /·/favicon.ico·/.well-known/security.txt 등으로 404 폭주를 일으킨다. compose 로그를 journald + logging.options.tag로 CONTAINER_TAG=orange-rest-api로 고정해 컨테이너 이름이 환경마다 달라도(고객사 orange-rest-api / dev rest-api) 한 벌의 jail이 모든 환경에서 동작하게 했다. banaction_allports로 시스템 방화벽 백엔드(nftables/iptables)를 자동 추종해 전 포트 차단, maxretry=10·bantime/findtime/ignoreip/bantime.increment는 [DEFAULT] 상속, recidive 메타 jail 연동. /api 404는 ignoreregex로 분리해 정상 요청을 살림. 짝지은 OR-1156은 client IP 위조 방어 — get_client_ip 신설(app/utils/client_ip.py). TCP peer가 공인이면(직접 연결) forwarded 헤더를 무시·peer를 사용, 사설/루프백(nginx 경유)이면 X-Real-IP 신뢰. 이로써 외부 스캐너가 XFF 위조로 ban 회피·무고한 IP 오폭하는 것을 차단. dev·test 서버 6/6 시나리오 PASS. 이번 주 완료.

Rule을 Git 서브모듈(Orange-Rules)로 분리 (OR-1457). 기존에 agent 레포 하위 폴더로 존재하던 rule을 독립 Git 서브모듈로 완전히 분리. 그동안 rule 관리자(데이터 분석자)가 로컬에서 관리하던 "최신 안정" rule을 package 레포의 rule/ 폴더에 수동 반영해 왔고, makepackage가 이를 제품 패키지에 복사했다. 이제 서브모듈의 main 브랜치가 항상 안정판을 보장하므로, package 레포는 더 이상 rule 사본을 보유하지 않는다. package/rule/ 폴더 전체 삭제(268개 파일), makepackage.sh의 rsync 소스를 $BASEDIR/rule/ → $BASEDIR/../rules/(루트 서브모듈)로 변경, makepackage.bat의 RoboCopy 소스도 %BASEDIR%rule → %BASEDIR%..\rules로 변경(CP949 + CRLF 인코딩 유지). 패키지 대상 경로({VER}/@update/rule/)는 변경 없음 — 에이전트가 기대하는 패키지 레이아웃 유지, 빌드 산출물 구조 동일. 빌드 전 동기화는 서브모듈별 git pull 또는 git submodule update --remote 방식이어야 하며, git submodule update만 실행하면 핀된 옛 커밋으로 되돌아간다(주의). 이번 주 완료.

sprocess hour 단위 schema 서버 측 적용 (OR-1342). OR-1340의 에이전트 작업과 짝을 맞춰 server 측 sprocess 컬렉션의 schema와 저장 로직을 hour 단위 통계 모델에 맞춰 개편. upsert 키 (id, hour, SUID)에 BootUID를 추가해 (id, hour, SUID, BootUID) — 재부팅 전후 데이터 last-write-wins 손실을 해소. hour/time 결정을 agent timestamp 기반으로 변경(1시간 align), agent ↔ server 시각 차이 3개월(90일)을 초과하면 server 시각으로 fallback, timestamp 음수 / 0 / None도 server 시각 fallback. fallback 발생 시 WARN 로그 + 1000건 단위 처리 통계에 fallback 카운터/비율 포함. 신규 마이그레이션에 기존 sprocess 인덱스 drop + 기존 sprocess 데이터 일괄 delete_many({})(신/구 의미 비호환) + 신규 인덱스 (id, SUID, BootUID, hour DESC) 재생성. app/services/sprocess_service.py 파일 전체 삭제(dead code), app/usecases/sprocess/get_sprocess_by_suid_case.py 삭제, /v3/sprocess/suid 엔드포인트 + 관련 import 제거, app/api/v2/v2_router.py의 /v2/sprocess에서 group_by_suid query param 및 aggregation else 분기 제거(manager-web은 항상 group_by_suid: false만 사용 중). 이번 주 추가 정리 4커밋, 완료.

이번 주 34커밋의 활동에는 추가로 OR-1465 노드 인증 critical path 슬림화 + 테스트 노이즈 제거(_make_case가 _fetch_auth_prerequisites 모킹), OR-1027 fastapi 0.135.3 → 0.136.3 lock 범프 + CVE-2026-48710(BadHost) Starlette 1.2.1 패치, OR-1153 MongoDB 설정 개선(WiredTiger 캐시·프로파일링·보안 강화), OR-1463/OR-1464 manager-web 하드코딩 한글/영문 i18n 정리(UX-021)도 포함됐다.


Baseline 일상 분류 서비스 Sprint 2 + 위젯 ACL 보안 사고 차단

Baseline 일상 분류 서비스 Sprint 2 — Override CRUD + audit_log + 검색 성능 가드 (OR-1438). OR-1438 본체에 운영자 강제 분류(Override) CRUD를 추가하고 BaselineLog를 OVERRIDE action으로 확장. populate_by_name 보강과 audit_log 적재(Sprint 2.2.1), baseline label endpoint 인증 명시 컨벤션 정리. baseline list 검색은 prefix-only 정책(성능 보강) — 와일드카드 전수 스캔으로 인한 부하를 차단. 운영 서버 detect_node 전체 이력(186,833행·206일)으로 도출한 5조건 룰(cv_count<0.15·k≥3·max_gap≤14·since≤14·rep≥0.6) 기반 자동 일상 분류 위에, 이번 Sprint는 운영자가 규칙으로 못 잡힌 케이스를 강제 분류하는 Override 레이어와 그 흔적이 영구히 남는 audit 인프라를 깐 것이다. 운영 원칙은 그대로 — 원본 컬렉션은 read-only(find만), 쓰기는 자체 컬렉션(baseline_routine · baseline_overrides · baseline_runs · baseline_labels)에만 upsert, 도메인별 독립 systemd(baseline-detect.service·baseline-system.service ...). 한 서비스 장애가 다른 도메인·기존 서비스에 영향이 없다. 이번 주 21커밋, 본 이슈의 최대 작업.

일상/비일상 전환 트래킹 — verdict_history 적재 + list sort 자동 전환 (OR-1452). baseline_routine doc에 verdict 전환 추적 필드(previous_verdict, verdict_changed_at, verdict_history, verdict_change_count)를 적재하고 응답·list sort에 자동 노출. 신규 키에서는 verdict_changed_at을 박지 않는 정책 보강(처음 보는 키에 "전환됨"이 표시되는 부자연 회피). 진행 중 bulk_upsert_routine path conflict(mongo error 40) 핫픽스도 포함. 운영자가 "이 키가 언제 일상에서 비일상으로 바뀌었는지" 시각적·API 양쪽에서 추적 가능. 이번 주 15커밋, 완료.

위젯 ACL 빈 배열 시맨틱 통일 — 보안 사고 경로 차단 (OR-1459). EndpointAcl.allowed_roles = []와 FeaturePermission.allowed_roles = []는 "전원 차단"인데, WidgetAcl.read/write/delete_roles = []만 "제한 없음(인증 사용자 전원 허용)"으로 정반대였다. 운영자가 위젯 권한 페이지에서 모든 역할 체크를 해제한 뒤 저장하면, 의도는 "이 위젯은 누구도 사용할 수 없게 차단"이지만 실제로는 WidgetAcl=[]로 저장되어 클라이언트가 length===0 → 허용으로 처리, 결과적으로 전원 허용된다. 보안 사고 경로다. 세 store 시맨틱을 통일했다 — WidgetAcl=[] → 전원 차단. 서버 docstring과 manager-web 3 파일(src/components/common/widgetPanel/index.tsx의 isWidgetAllowed, src/components/widgets/DynamicComponentLoader.tsx의 isWidgetDenied, src/components/widgets/setting/index.tsx의 isCategoryAllowed)을 모두 length===0 → 차단으로 통일. widgetAclTab의 신규 위젯 자동 전체 체크(isNew ? [...allRoleIds]) UX는 그대로 유지 — 신규 위젯 추가 → 권한 페이지 진입 → 그대로 저장 시 모든 역할 명시 저장 → 전원 허용으로 동작(권한 누락에 따른 자동 차단 회피). 운영 적용 전 mongo 마이그레이션 필수: 기존 DB의 빈 배열 위젯이 시맨틱 변경 적용 즉시 차단되어 일반 매니저의 위젯 접근이 차단되므로, 빈 배열을 모든 역할로 채우는 console 명령(is_root / super_admin은 항상 우회되므로 관리자 본인 접근은 영향 없음)을 함께 제공. super_admin / is_root 우회는 변경 전과 동일하게 모든 ACL 검사를 통과. 이번 주 4커밋, 진행 중.


Agent 커널 드라이버 IL 파이프라인 + MS 서명 자동화 + BinSnap

orange.sys Y_PROCESS_MESSAGE Integrity Level end-to-end 파이프라인 (OR-1467). 프로세스의 무결성 수준(Integrity Level, IL)을 ofilter(커널) 단계에서 수집해 Y_PROCESS_MESSAGE에 담아 Agent로 전송하고, Agent에서 수신한 IL을 매니저 JSON으로 전달하는 end-to-end 파이프라인을 깔았다. IL은 S-1-16-<RID> 형태의 SID로 표현되며 마지막 sub-authority(RID)가 곧 IL 값이다. SECURITY_MANDATORY_UNTRUSTED_RID 0x0000 / LOW 0x1000 / MEDIUM 0x2000 / MEDIUM_PLUS 0x2100 / HIGH 0x3000 / SYSTEM 0x4000 / PROTECTED_PROCESS 0x5000 — RID가 0x1000 단위로 단조 증가하므로 대소 비교만으로 등급 판정이 가능하다.

커널 측 구현은 CProcessTable.h에 GetProcessIntegrityLevel(PEPROCESS) 헬퍼를 신규 추가. PsReferencePrimaryToken 참조 → SeQueryInformationToken(TokenIntegrityLevel) 조회 → PsDereferencePrimaryToken 해제. 조회 실패 / 토큰 없음 / IRQL > PASSIVE 시 Medium(0x2000) fallback. CProcessTable.h::Add3()에서 엔트리 삽입 직전(UpdateEntryInfo 이후, 테이블 Lock 이전, PASSIVE 구간)에 entry.IntegrityLevel을 채웠다 — 모든 수집 경로(신규 생성, 부팅 시 기존 프로세스, port 요청)가 단일 지점에서 자동 반영된다. ofilter.h의 PROCESS_ENTRY에 DWORD IntegrityLevel 필드 추가, Message.cpp / ofilter.h의 CreateProcessMessage에 IntegrityLevel 전달 추가(pEntry 오버로드는 자동 전파, 명시 인자 오버로드는 DWORD 파라미터 추가). 이번 주 완료.

File 오류 시간 기반 throttle + LRU + 시스템 바이너리 화이트리스트 확장 (OR-1427). 한 달간 17조 회 누적 관측된 File.Failed.cpp / 삭제 이벤트의 분당 39만 건 driver → agent notify 폭주에 대한 본격적인 throttle 정책을 적용. 기존 count % 100 == 1(동일 process+status+file key마다 매 100회 1회 통과)은 한 key가 지속 폭주하면 100건당 1건씩 무한 통과되어 폭주 자체를 막지 못했다. KeQueryUnbiasedInterruptTime(절전 시간 제외, 100ns 단위) 기반 10분당 1회로 전환 — 노트북이 절전 상태였던 시간은 카운트 안 되어 깨어난 직후 즉시 notify 폭주를 막는다. 17조 회 케이스 기준 notify 수가 1,700억 건에서 4,320건/key로 줄어 약 4천만 배 절감.

자료구조도 같이 갈았다. 기존 hash table(open addressing) + ERESOURCE(56B)는 포화 시 RtlZeroMemory로 전체 reset → 모든 key의 LastNotify 손실 → 다음 발생들이 "첫 발생"으로 인식되어 즉시 notify 통과 → throttle 무력화 위험이 있었다. FILTER_COUNT_TABLE을 512 entry linear array + EX_PUSH_LOCK(8B, 1/7 크기)로 교체 — open addressing의 tombstone / chain 깨짐 위험 회피, contention 없으면 atomic CAS만. 포화 시 동작은 전체 reset에서 가장 오래된 LastNotify entry 1개만 LRU evict로 교체 — 다른 entry의 LastNotify를 보존해 throttle 효과 유지. linear scan 한 번에 기존 key 검색 + oldest 추적을 동시 처리. API도 단순화 — 기존 FilterCountIncrement가 호출자 측 count % N == 1 분기 처리를 강요하던 것을 FilterCountIncrementAndCheckInterval(Key, IntervalSeconds, &count) 한 줄로 통합 (TRUE이면 호출자가 FfNotifyFailed 호출).

SIGNER_CACHE도 같이 정리. MS 서명 프로세스 필터링 초기 설계에서 PE Authenticode(DER PKCS#7) 파싱으로 서명자 CN 추출을 검토했으나 매 프로세스마다 디스크 I/O 3회 + PagedPool 할당이 비현실적이라 PE 파싱을 제거하고, pPsGetProcessSignatureLevel 결과를 ProcPath 키로 LRU 캐시(SIGNER_CACHE)로 전환. PE 파싱은 매 호출 수 ms·PagedPool 최대 16MB·부팅 시 50-100 spawn × 수십 KB I/O인데, SIGNER_CACHE는 spinlock + atomic CAS만 — 매 호출 마이크로초 단위.

WindowsSystemBinary 화이트리스트도 확장. 기존 단일 변수(g_SystemAppsPrefix)를 배열 + 루프 매칭으로 변경. 8개 prefix(\Windows\SystemApps\, \Program Files\WindowsApps\, \Program Files\Microsoft OneDrive\, \Program Files (x86)\Microsoft\, \Program Files\Microsoft Office\, \Program Files (x86)\Microsoft Office\, \Program Files\Common Files\microsoft shared\, \ProgramData\Microsoft\Windows Defender\Platform\)에 24개 basename(WmiPrvSE.exe·MsMpEng.exe·Office 8종·OneDrive 7종·Edge WebView2 등) 추가. cmd.exe는 Debug 빌드에서만 화이트리스트 제외(#ifndef DBG), Release 빌드에서는 시스템 바이너리로 포함. 이번 주는 인코딩 정리 1커밋.

MS 서명 자동화 빌드 서버 내장 — 비동기 Job 패턴 (OR-1437). 현재 orange.sys 드라이버의 MS 인증 서명(Attestation Signing) 프로세스는 개발자 PC에서 ms-attest\ms_attest.py를 수동 실행해야 한다. credentials.json(Azure AD 시크릿)을 개발자 PC마다 관리하고, Python venv + msal/requests 패키지를 설치하고, make_driver_cab2.bat(CAB 생성/EV 서명)과 ms_attest.py(MS 인증서명)가 분리되어 2단계 수동 작업이 필요했다. 코드사인 빌드 서버(공인 IP의 :44 → 내부 192.168.1.51:80, Caddy reverse proxy → FastAPI :8080)에 내장해 CAB 파일을 업로드하면 MS Partner Center 제출-폴링-다운로드까지 서버에서 자동 처리되도록 변경.

/sign(eToken EV 서명)은 수 초·동기 응답인 반면 /ms-attest는 최대 60분이 걸리므로 Job 기반 비동기 패턴으로 설계. POST /ms-attest는 CAB 업로드 → job_id 즉시 반환(HTTP 202) + asyncio.create_task로 백그라운드 워커 시작(MSAL 토큰 발급 → Product 생성 → Submission 생성 → Upload URL 탐색 → Azure Blob 업로드 → Commit → 30초 간격 폴링(60분 타임아웃) → Signed package URL 다운로드). GET /ms-attest/{job_id}는 상태 조회(pending/completed/failed), GET /ms-attest/{job_id}/download는 completed 시 signed zip을 FileResponse로 반환. 상태는 파일 기반 JSON(jobs/{job_id}.json)으로 영속 — submitted / pending / completed / failed / interrupted 5상태. 서버 재시작 복구: startup 시 submitted/pending 상태의 job을 interrupted로 마킹. 각 단계마다 job state JSON 업데이트, 실패 시 Slack 알림 + audit 로그. Caddy 라우팅에 /ms-attest와 /ms-attest/* 추가, msal>=1.28.0을 requirements.txt에 추가(requests 대신 기존 httpx 활용). 이번 주 완료.

BinSnap — Windows 업데이트 시스템 프로세스 diff 자동 감지 도구 (OR-1445, OR-1443). Windows 업데이트로 새로 박힌 시스템 프로세스를 자동 추적하는 독립 도구. 초기 설계의 작업 스케줄러 이벤트 트리거는 Microsoft-Windows-WindowsUpdateClient/Operational 채널의 EventID 19(설치 성공)였고, schtasks 등록 검증까지 통과했다. 그런데 실제 개발 머신(Windows 11)에서 EventID 19를 조회하니 결과가 0건. WindowsUpdateClient/Operational 채널에 EventID 19가 4개월간 1건도 없었다(25/26/41만 존재). 그러나 실제로는 설치가 되어 있었다(Get-HotFix: KB5089569·KB5092427·KB5089583 등 5/29~5/30). 즉 19 트리거만으로는 이 머신에서 WU 감지가 영영 동작하지 않았을 것이다.

원인: 현대 Windows 11은 누적 업데이트(LCU)를 CBS(Component-Based Servicing) 경로로 설치한다. 이 경로는 EventID 19를 남기지 않고 Setup 채널 / Microsoft-Windows-Servicing Provider / EventID 2를 남기며, 설치 주체는 Client = UpdateAgentLCU다. Get-HotFix 교차 검증으로 실제 설치가 EventID 2로 기록됨을 확인. 신규 트리거 XML BinSnap-OnServicing.xml을 추가하고 성공한 설치만 거르도록 UserData 조건까지 XPath에 포함: *[System[Provider[@Name='Microsoft-Windows-Servicing'] and (EventID=2)]] and *[UserData/CbsPackageChangeState[IntendedPackageStateTextized='Installed' and ErrorCode='0x0']]. 결과는 트리거 3개 체계 — 정기 백업(매주 일요일 03:00), 레거시 WU(EventID 19, 보험), 현대 CBS/LCU(EventID 2, 이 머신 실제 경로). 두 이벤트 트리거 모두 SYSTEM/Highest, Delay PT5M, IgnoreNew — 한 번의 업데이트로 EventID 2가 여러 번 발생해도 1회만 실행. 환경 무관 커버. 교훈: 문서(MS Learn)만 봤으면 "EventID 19 사용"으로 끝나 이 머신에서 동작하지 않았을 것이다. 실제 머신의 이벤트 로그를 직접 조회/교차검증한 덕에 CBS 경로(EventID 2)를 발견했다.

부가로 UpdateInfo 모듈을 추가해 "어떤 업데이트(KB) 때문에 시스템 프로세스가 추가됐는지" 인과를 diff에 함께 기록. EventID 19의 KB는 EventData의 Data Name="updateTitle", EventID 2의 KB는 UserData의 PackageIdentifier — 구조가 달라 두 채널 동시 조회. --list-updates=10080 헬퍼(스캔/diff 안 거치고 KB 조회만 단독 실행, 7일치). EvtQuery 15001(ERROR_EVT_CHANNEL_NOT_FOUND) 이슈는 simple XPath의 Provider[@Name='Microsoft-Windows-Servicing'] 필터를 EvtQuery가 채널로 해석하려다 Path 인자("Setup")와 충돌하는 문제로, structured query(트리거 XML과 동일한 <QueryList><Query Path=...> 형식)로 우회. EvtQuery(NULL, NULL, structuredQuery, EvtQueryChannelPath | EvtQueryReverseDirection). 이번 주 완료.

Agent 설치본 패키징 전 Rule(Git) 최신화 자동화 (OR-1456). Agent 설치본 패키징 시 Rule 데이터를 로컬 working copy에 의존하지 않고 Rule 전용 저장소(Orange_Rules)에서 매번 최신 상태로 가져와 패키징. 기존엔 메인 저장소(Orange-Agent) working copy 내 Rule 폴더를 그대로 사용해 working copy 상태(미커밋/untracked 파일 등)에 따라 패키징 결과가 달라졌다. codesign/package2.bat이 NSIS 호출 직전에 스크립트 자기 경로(%~dp0 = codesign)에 Orange_Rules로 얕은 clone(이전 clone 있으면 먼저 삭제) → clone 실패 시 패키징 중단(stale rule로 빌드되는 사고 방지) → clone의 .git 제거 → 기존 Rule 폴더를 통째로 삭제 후 clone 내용 복사(완전 교체) → 임시 clone 폴더 삭제(git working tree에 흔적 없음). NSIS 스크립트(binary/package/orange.nsi)는 변경 없음 — 기존대로 repo 루트의 Rule 폴더를 읽음. 완전 교체 방식이므로 working copy의 untracked rule(예: Rule/0005/@0002)은 패키징 시 제거되며, 필요한 rule은 Orange_Rules 저장소에 반드시 커밋되어 있어야 한다. 이번 주 완료.


Manager Web 디자인 시스템 정립 + 위젯 전면 개선

디자인 시스템 가이드라인을 새로 작성하고, 그 가이드라인을 다섯 위젯에 동시 적용하면서 dead code도 같이 비운 한 주다.

Manager Web 디자인 시스템(DESIGN_SYSTEM.md) 설계 (OR-1455). Orange Manager Web 신규 컴포넌트 개발 시 준수해야 하는 디자인 규칙 전체를 단일 문서로 정립. 기술 스택은 React 19 + Vite + Chakra UI v3 + Tailwind CSS — 모든 색상 토큰은 Tailwind 클래스명과 Hex 값으로 이중 정의된다.

브랜드 색상은 Orange 계열로 선택 강조 · 체크박스 · 포커스 링 전용, 차트 시리즈에는 절대 사용 금지. orange-500 #f97316(Primary, 선택 강조·체크박스·포커스 링), orange-400 #fb923c(트리 토글 화살표·드롭 위치 테두리), orange-100 #ffedd5(선택된 행 배경·그리드), orange-50 #fff7ed(선택된 행/항목 배경·트리 선택). 차트/카테고리 색상은 색상환 균등 배분 8색 메인 + 8색 서브 — Blue(225°) → Violet(265°) → Fuchsia(294°) → Rose(346°) → Amber(38°) → Lime(83°) → Emerald(157°) → Cyan(196°). 9번째 이상은 Sky·Indigo·Purple·Pink·Yellow·Green·Teal·Slate로 순환. TypeScript 상수로 CHART_COLORS_PRIMARY · CHART_COLORS_SECONDARY · CHART_COLORS · getChartColor(index) 통합 export.

상태 태그는 두 체계로 명확히 분리. SAFETY_TAG는 "이 값이 정상인가?" — safe: teal-600·warning: amber-500·danger: red-600로 감정적 판단(접속 허용·정상/잠금·주의/접속 비허용·오류). LEVEL_TAG는 "이 값이 얼마나 많은가?" — low: sky-500·medium: sky-700·high: blue-900로 쿨 블루 계열 중립적 단계(디스크 10%/50%/90%, 메모리 낮음/중간/높음). 두 체계 차이의 핵심은 "수치가 높다고 이상한 것이 아닌, 단순 값의 레벨을 나타냄"이라는 명시.

버튼은 Chakra UI prop으로 colorPalette="black"(주요 액션, 저장·추가·적용), colorPalette="red"(위험 액션, 삭제), colorPalette="blue"(보조 액션, 잠금·연결 고정), variant="outline"(취소·닫기). 텍스트 스타일 — 섹션 제목 text-gray-700 font-semibold text-base, 본문 text-gray-800 font-semibold, 보조 설명 text-gray-700 font-normal. text-gray-600 이하를 일반 텍스트에 사용 금지, 최소 text-gray-700 이상 사용(아이콘 className에만 예외). 공통 UI 패턴(검색 바, 태그/칩, AG Grid CSS 변수)과 아이콘 시스템(react-icons 우선 + FontAwesome fas)도 함께 표준화. CLAUDE.md · CLAUDE_WIDGET.md · CLAUDE_PLAYWRIGHT.md도 같이 정비. 이번 주 진행 중.

프로세스 영향도 위젯 UI/UX 전면 개선 (OR-1466). 17파일 +1,527/-1,160 규모. ListView 완전 재설계 — 표(thead/tbody)에서 왼쪽 사이드패널(회사별 / 프로그램명 탭 + 그룹 트리 선택) + 오른쪽 카드형 목록으로. CPU 진행바는 단색 오렌지에서 LEVEL_TAG 3단계 퍼센타일 기반(현재 뷰 내 순위)으로 — 상위 40% high → blue-900, 상위 40~70% medium → sky-700, 하위 30% low → sky-500. 필터는 TOP10/전체 칩 2개에서 회사별/프로그램명 탭 + 그룹 트리로 확장. 검색은 onChange 즉시 필터에서 Enter 확정 / 미커밋=blue 테두리+↵ 힌트 / × 초기화로 변경(미커밋 상태 피드백 패턴). 페이지네이션은 없음 → 10/20/30 선택 + Chakra Pagination. 빈 상태는 데이터 없음(📡 아이콘) / 검색 결과 없음(🔍 아이콘) 구분.

DetailView 헤더는 한 줄([←] chrome.exe (flex-1, truncate) [📄])로, 메타 정보는 수직 나열에서 4열 CSS grid로 — 설명/회사/제품/파일 + 최초 발견 + MD5/SUID. PDF 버튼은 텍스트 "PDF 출력"에서 fas file-pdf 아이콘 + Tooltip(disabled 시 opacity-25)으로. 초기 뷰 범위 ±30일(61일)에서 ±14일(29일)로 축소. hourly 범위는 단일 날짜 24시간 고정에서 드래그 구간 전체(N일 × 24h)로 확장 — hourlyToDay 상태 추가.

Timeline 차트는 대폭 개선. 이벤트 dot lane(투명도로 강도)을 GitHub 잔디 스타일 heatmap(5단계 셀·색상 범례 '적음 □□□□□ 많음', EVENT_ORDER 초과 시 ▲▼ 스크롤)로 전환. 차트 전체 높이 920px → 684px, 이벤트 heatmap 영역 220px → 104px, X축 36px → 12px. 마우스 휠 동작이 핵심 변경 — 일반 스크롤은 차트 포커스 시 좌우 날짜 이동(포커스 없으면 페이지 스크롤 위임), Ctrl + 스크롤은 확대/축소(daily↔hourly 전환 포함), trackpad 좌우 스크롤은 deltaX 감지 → 날짜 이동, 히트맵 행 스크롤은 히트맵 영역 위 스크롤 → 행 스크롤(오버플로 시). 신규 기능: 포커스 아웃라인(SVG 클릭 시 orange 2px, 차트 밖 클릭 시 자동 해제, mode 전환 시 초기화), Catmull-Rom 곡선 스무딩(smoothCatmullRom 함수 추가). hourly 라벨 자동화 — ≤6h=1h간격 / ≤12h=2h / ≤24h=4h / ≤48h=6h / ≤96h=12h / >96h=24h. 노드 확산 바 필터링(오늘 이후 + firstSeen 이전 날짜 데이터 렌더링 제외), 색상 #0369a1 → #2563eb.

useChartDataApi — Multi-day Hourly 지원. hourlyToOffset 파라미터 추가(끝 day 오프셋, null이면 단일 day), daily만 2회 분할이던 것을 hourly도 31일 초과 시 자동 2회 분할(서버 max 31일 우회). allDates는 hour 인덱스(0~23)에서 'YYYY-MM-DD HH:00' 통합 슬롯 키로. 차트 색상 팔레트 변경 — CPU cyan-400 #06b6d4 → violet-700 #7c3aed(orange 계열 시각 충돌 방지), MEM violet-500 → fuchsia-600(CHART_COLORS_PRIMARY 팔레트 정렬), Handle amber-400 → rose-600(orange 계열 충돌 방지), IO emerald-500 → amber-600. widgetTemplate에 forceGrayTopBar?: boolean prop 추가 — 위젯 선택 상태 무관하게 상단 컬러 바를 회색 고정(ProcessImpactMap 내부 ListView/DetailView가 자체 색상 체계를 가지므로). 이번 주 진행 중.

설정 위젯 + moreVisualEffect dead code 정리 — 21파일 -3,947 라인 (OR-1453). 설정 위젯(setting/) 전체와 공통 위젯에 대해 DESIGN_SYSTEM.md 기준 브랜드 색상 통일과 함께 21파일 +664 / -3,947(약 86% 순감) dead code 일괄 제거. moreVisualEffect 체크박스 UI(Style.tsx 파일 삭제), style 폼 필드 제거(manager/generalTab/index.tsx), isVisualMode 참조 전 파일에서 제거. useGlobalStore 구조분해 9개 항목 미사용 → 실제 사용 항목만, useForm 구조분해 register/reset/setError/clearErrors/trigger/isSubmitting 미사용 → 실제 사용 항목만(setValue/getValues/control/errors 등). 미사용 import — Switch · ButtonGroup · Pagination · dayjs/utc · TreeNode · AgGridCellBarChart 등 전체 제거. 빈 콜백/useEffect(onSettled(): void { // } / useEffect((): void => { // }, [])) 전체 제거. Switch.Root 주석 블록 — Checkbox 전환 후 남겨진 잔재(Manager 7개, Agent 2개, Accounts 1개) 전체 제거. Dead branch — const isNew = false; if (isNew) { fetch POST ... }(절대 실행 안 됨)을 PUT 직접 호출로 단순화. 미사용 변수 — iconRef(DOM 미연결), rectX(선언만), watchedSelectedIconPagination 등 전체 제거. 대형 주석 코드 블록(checked-delete case, 이름 입력 필드, 그룹 필드, 코드자동생성 UI 등 JSX 블록) 전체 제거.

UI 색상도 함께 정비. 탭 사이드바 선택 UI는 bg-gray-700 → bg-orange-50 + border-l-2 border-l-orange-500, 탭 아이콘 배경 bg-gray-600 → bg-orange-500(선택) / bg-orange-400(비선택). 텍스트 색상 text-gray-400/500/600 → text-gray-700 이상(DESIGN_SYSTEM 최솟값). 저장 버튼 아이콘 fas download → MdOutlineSaveAlt, 버튼 팔레트 colorPalette="black" 명시 적용. 검색 input 포커스 색상 border-gray-200 / focus:border-gray-400 → border-gray-300 / focus:border-orange-400 focus:ring-1 focus:ring-orange-400. 계정탭 검색은 단독 input(돋보기 아이콘 내장, Enter 검색)으로 표준 검색바 패턴 적용, 페이지당 10/20/50개 pill 버튼 추가(rows_per_page 파라미터 연동·페이지 1 리셋), 잠금=amber·접속비허용=red 배지. 일반탭 체크박스에 Chakra UI v3 prop으로 colorPalette="orange" 적용(기존 className="chakra-checkbox__control"은 v3에서 미동작). GridPagination(common) — 현재 페이지 border-blue-500 bg-white → bg-orange-500 text-white, hover hover:border-blue-300 → hover:border-orange-300 hover:bg-orange-50. processInfo / FileTab·RunTab — 더보기/체인 버튼 text-blue-600 → text-orange-500. serverMonitor / reportViewer hover — hover:bg-blue-50/50 → hover:bg-gray-100. ag-theme-alpine 전역 — --ag-row-hover-color: #f3f4f6, --ag-selected-row-background-color: #ffedd5, --ag-checkbox-checked-color: #f97316. 이번 주 완료.

조직도 위젯 DnD 3단계 + 사용자 CRUD 즉시 저장 + 화면 위치 유지 (OR-1447). @rc-component/tree 라이브러리 내장 DnD + 커스텀 DOM 이벤트 리스너 혼용으로 초록/파랑 색상 충돌 · 드롭 위치 부정확하던 트리 DnD를 커스텀 div 렌더러로 전면 교체. 마우스 Y 위치 기준 before(상위 30%) / into(중간) / after(하위 30%) 3단계 위치 인식. 최상위 노드 간 이동 차단. 드롭 타겟 연주황(bg-orange-100) 하이라이트 통일. 편집 모달 DnD에서도 동일 적용 — before/after 시 주황 상하단 테두리 표시, 최상위에서 before/after 시도 시 into로 강제. 우클릭 컨텍스트 메뉴는 완전 제거(우클릭 기본 동작 차단, 조직 구조 편집은 '조직도 편집' 버튼 → 편집 모달로 통합).

사용자 저장 방식 — 추가/수정/삭제 후 로컬 상태만 변경 → 반드시 메인 저장 버튼 클릭 후 API 전송이던 것을, 추가/수정은 확인 즉시 API 저장, 삭제는 경고 확인 다이얼로그 후 즉시 API 저장으로 변경. 메인 저장 버튼은 트리 구조 변경 시에만 사용. 사용자 추가 폼 — 현재 선택된 그룹이 자동 설정·변경 불가에서 조직 선택 드롭다운 추가(전체 그룹 목록 표시, 기본값=최상위 첫 번째 그룹), 모든 필드 필수(*) 표시, 미입력 시 추가 버튼 비활성화로 개선. 저장 후 화면 처리 — window.location.reload()(전체 페이지 새로고침, 홈으로 이동)에서 reset() + organizationDataRefetch() + rebuildUserNodes()로 데이터만 재조회 · 현재 화면 위치 유지로 변경. 그리드 색상 — AG Grid alpine 기본값(호버 연파랑·선택 파랑·체크박스 파랑)에서 호버 #f3f4f6(연회색) / 선택 #ffedd5(연주황) / 선택+호버 #ffe0b2 / 체크박스·셀 포커스·아웃라인 #f97316(주황) — CSS 변수 + !important 이중 오버라이드로 안정 적용. 그리드→트리 드래그 후 목록 갱신 — 드롭한 노드를 selectedTreeNode로 변경 → 기존 useEffect 자동 실행 → 해당 그룹 사용자 목록 즉시 갱신. 트리 토글 아이콘 text-blue-500 → text-orange-400(메인 트리 + 편집 모달 트리 모두). 이번 주 진행 중.

서버 모니터링 위젯 색상·아이콘 정비 (OR-1458). 서버 모니터링 위젯(serverMonitor) 전체에 걸쳐 DESIGN_SYSTEM 및 CLAUDE.md UI 규칙 준수. CLAUDE.md '회색 글씨 금지' 규칙(text-gray-600 이하 일반 텍스트 금지, 최소 text-gray-700 이상)을 위반하던 slate-* 계열 클래스를 gray-* 계열로 전면 교체. 상태 색상은 SAFETY_TAG 컨벤션 통일 — healthy/정상 green/lime → teal(bg-teal-50 / text-teal-600 / bg-teal-500 dot), warning/점검 필요 yellow → amber(bg-amber-50 / text-amber-500 / bg-amber-400 dot), error/위험 red-500 → red-600. 탭 스타일 표준화 — 선택 탭 border-orange-500 text-orange-600, 미선택 탭 text-gray-700 hover:text-orange-500 hover:bg-orange-50 rounded-t-md, font-bold → font-semibold.

FontAwesome 아이콘 이름 정정 — question-circle → circle-question(FA Pro 올바른 이름, ConnectedDevicesTab 미분류 그룹 아이콘), radar → satellite-dish(최초 스캔 이벤트 아이콘), router → ethernet(SNMP 대상 아이콘, RegisterTargetModal 및 TYPE_ICON 상수). TYPE_ICON 상수 확장 — 모니터링 대상 타입별 고유 색상 체계 도입 + activeBorder 및 hex 필드 추가. http blue-600 #2563eb / tcp violet-600 #7c3aed(기존 slate-500) / db fuchsia-600 #c026d3(기존 violet-500) / ssl rose-600 #e11d48(기존 emerald-500) / dns amber-600 #d97706(기존 cyan-500) / snmp lime-600 #65a30d(기존 amber-600, 아이콘 ethernet). OrangeServerTab 인프라 카드 색상 재배치 — 탭 전체 배경 투명 → bg-gray-50, 리소스 카드 CPU blue-50 · Memory violet-50(기존 pink) · Disk fuchsia-50(기존 green) · Uptime rose-50(기존 indigo), 인프라 서비스 MongoDB emerald-50 · Redis cyan-50(기존 red) · MQTT sky-50(기존 yellow), 네트워크 I/O amber-50(기존 cyan) · API서버 lime-50(기존 violet). CompactServerStatus / OrangeServerTab의 wsConnected 변수 명시적 추출(const wsConnected = !!wsData && !wsError). 이번 주 완료.


분석 R&R 본업

프로세스 영향도 맵 위젯 mockup_v6 매니저 위젯 포팅 (OR-1293). 데이터 분석에서 검증한 프로세스 영향도 맵 mockup_v6 디자인을 매니저 위젯으로 포팅. 본 티켓의 원래 요청은 "system 컬렉션 시간당 1건 강제 수집" 강화였으나, 이후 데이터 분석에서 발상 자체가 잘못됐음이 확인되어 폐기 처리되었다(Won't Do).

폐기 이유 — system 컬렉션은 (PC × 5분) 의 OHLC + 누적 통계 + 그 5분간 이벤트 카운트로, 본질이 "5분 단위 압축"이다. "시간당 1건 강제"는 5분 단위 의미를 파괴하는 발상이다. 그리고 sprocess가 이미 PC × 프로세스(SUID) 단위 시간당 1건 시계열을 추적 중이다 — 한 (PC, SUID)의 측정 분포 검증에서 174일 range에서 2,700 unique 5분 slot, docs/hour = 1.0. 즉 PC가 깨어있는 동안 한 SUID = 시간당 1건 정확히 측정. system 강화 안 해도 sprocess로 시계열 baseline 산출 가능하다. 그리고 진짜 부족함은 데이터 양이 아니라 PC 가동 시간이다 — system이 sparse한 진짜 원인은 PC가 켜져 있는 시간이 적은 것(출근 8h / 퇴근 16h)으로, Agent 측 수집 강화로는 해결이 안 된다.

A+D prototype 검증 결과로 폐기를 뒷받침했다. chrome.exe 도입 ±14일·임계값 10건·sprocess+system 5분 슬롯 합집합 기준에서, system 단독(±7일·≥50건)이면 가능 PC 5개(11%)였던 것이 A 조합(±14일·≥10 슬롯·union)이면 21개(46%)로 4배 확장. 시스템 강화 없이 임계값 + 윈도우 + 합집합 조정만으로 가능 PC가 4배 확장된다는 결론. 신뢰도 100% 결론을 위한 진짜 데이터 갭(★★★ PC 가동/슬립 이력 명시 — Agent boot/shutdown 이벤트, ★★★ 사용자 활성 시간 — 키보드/마우스 input·포어그라운드 윈도우, ★★★ 변경 이벤트 통합 타임라인 — filelist + nodeinfo 변동 + OS 패치, ★★ 애플리케이션 그룹핑 — chrome 계열 매핑, ★★ 사용자 변경 이벤트)도 별도 ticket 후보로 분리해 정리. 이번 주 1커밋(매니저 위젯 포팅).


마무리

이번 주 팀 합산 117커밋. 코드량으로는 Baseline Sprint 2 + 위젯 ACL 시맨틱 통일과, 프로세스 영향도 위젯 서버 4-레포 + MQTT $SYS 4 토픽 + fail2ban HTTP jail + Rule 서브모듈 분리 + sprocess hour schema가 양대 축이고, 디자인 시스템 정립 + 위젯 5종 개편 + dead code 정리도 그에 못지않은 규모로 따라붙는다. 커널 드라이버 파트는 Y_PROCESS_MESSAGE IL 파이프라인·MS 서명 자동화·BinSnap·Rule 최신화 자동화로 커널 드라이버·코드사인 인프라를 동시에 짚었고, 매니저·에이전트 파트는 sprocess hourly의 stall 회귀를 28시간 #if 0 격리로 잡아내고 Agent Server 시나리오 통합으로 외부 단말 자가 복구 경로를 복원했다. 룰·데이터 분석 파트는 mockup_v6 위젯 포팅과 함께 폐기 결정 정리가 더 큰 산출이었다.

다음 주 축은 (1) sprocess hourly + SystemStat MQTT 송신 메시지 스키마 합의 후 안정 운영 진입, (2) 프로세스 영향도 위젯의 In QA → 출시, (3) Baseline Sprint 3(다도메인 확장: system/process/nodeinfo), (4) Manager Web 디자인 시스템 적용을 다른 위젯들로 확산.