핀타에이아이
경력근거: 실행 맥락을 바탕으로 위험한 행동을 탐지·차단하는 AI 보안 제품을 만들고 있습니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
핀타는 AI 에이전트가 어떤 도구를 사용해 무엇에 접근하고 어떤 작업을 수행하는지 관찰하고, 실행 맥락을 바탕으로 위험한 행동을 탐지·차단하는 AI 보안 제품을 만들고 있습니다. 이 공고의 FDE 는 고객 환경에서 수집 서버 배치, 에이전트 트래픽 연동과 탐지 룰 조정을 직접 수행하고, 고객사의 IdP, 기존 보안 제품과 배포 파이프라인에 제품을 연결합니다. 에이전트의 도구 호출은 MCP 서버 계정으로 성공하므로 시스템 접근 로그만으로는 권한 침해가 드러나지 않고, 호출 기록을 IdP 그룹과 접근 정책에 견줘야 시킨 직원이 볼 수 없는 자원이 보입니다. 이 화면은 그 견줌을 PoC 결과로 내놓는 자리라, 공고가 든 CISO, 보안 엔지니어링 리더와 AI 플랫폼 담당자에게 탐지 방식의 근거를 설명하는 일과 오탐과 성능 등 검증 질문에 대응하는 일에 그대로 쓰입니다. 공고가 밝히는 것은 수집 서버 배치와 IdP 연결까지라, 수집 서버가 자원을 hr/·secret/prod/ 같은 경로로 남기는 모양과 고객사 접근 정책이 그룹별 허용 경로 표로 오는 모양은 지원자가 정해 로컬 Keycloak 내보내기와 엑셀 모양 표로 만듭니다. 이 포트폴리오는 도구 호출 수집, IdP 그룹 짝짓기, 세션 단위 탐지 룰을 Python 으로 엮어 호출마다 근거를 남기는 판정 엔진과 결과 화면을 보여줍니다. 금융권·대기업 보안 규제나 침해 사고 대응 전문성은 주장하지 않습니다.
공고는 핀타의 제품을 AI 에이전트가 어떤 도구를 사용해 무엇에 접근하고 어떤 작업을 수행하는지 관찰하고, 실행 맥락으로 위험한 행동을 탐지·차단하는 보안 제품으로 소개합니다. 도구 호출 한 건을 시킨 직원, MCP 서버, 자원, 세션으로 풀어 판정하는 이 화면이 그 관찰의 가장 작은 단위입니다.
주요 업무에 수집 서버 배치, 에이전트 트래픽 연동과 탐지 룰 조정을 직접 수행한다고 적었고, 고객사의 IdP 와 배포 파이프라인에 제품을 연결하며 필요한 스크립트와 설정을 직접 작성한다고 했습니다. 호출 수집, IdP 구성원 짝짓기, 세션 창 룰이 이 포트폴리오의 세 축입니다.
업무 비중의 약 40%가 고객 대면이고, CISO, 보안 엔지니어링 리더와 AI 플랫폼 담당자에게 탐지 방식의 근거를 설명하며 오탐 질문에 대응한다고 적었습니다. 펼친 줄마다 그룹 경로와 세션 흐름을 남기는 화면은 그 설명을 PoC 결과 회의에서 바로 쓰는 모양입니다.
보안 엔지니어가 PoC 기간을 고르면 이 화면은 핀타 수집 서버가 모은 에이전트 도구 호출마다, 호출을 시킨 직원을 고객사 IdP 구성원과 짝지어 그 직원의 그룹이 허용하는 자원 경로와 견줍니다. MCP 서버 계정으로 호출이 성공했어도 시킨 직원의 그룹 경로 밖이면 권한 밖 자원, 승인 목록에 없거나 승인 보류인 MCP 서버면 등록 안 된 서버, IdP 에 없거나 비활성화 뒤 호출이면 IdP 에서 빠진 사용자, 같은 세션에서 민감 경로를 읽고 10분 안에 외부 전송 도구를 부르면 민감 자원 외부 전송으로 판정합니다. 보안 엔지니어와 CISO 는 PoC 결과 회의에서 이 판정을 펼쳐 보며 탐지 근거가 맞는지와 도입 여부를 정합니다.
핀타 제품을 PoC 로 들여 본 고객사의 보안 엔지니어와 CISO. 사내에 코딩 에이전트와 MCP 서버가 늘었는데 기존 접근 로그로는 에이전트가 누구 대신 무엇을 읽었는지 알 수 없어, PoC 결과 화면에서 걸린 호출을 함께 보며 탐지 근거가 맞는지, 도입할지를 정한다
고객사의 기존 접근 로그에는 hr-db 를 읽은 주체가 MCP 서버 계정 svc-hr 로만 남아서, 인턴이 시킨 에이전트가 급여 표를 읽어도 로그로는 정상 서비스 호출과 구별되지 않습니다. 에이전트 수집 서버가 남기는 호출 기록에는 시킨 직원과 세션이 있으므로, 그 직원을 IdP 그룹과 접근 정책에 견주고 같은 세션의 앞뒤 호출을 이으면 권한 밖 읽기와 읽은 뒤 밖으로 보낸 흐름이 호출 단위로 드러납니다. CISO 는 이 판정을 근거와 나란히 보고 PoC 결과를 판단합니다.
지원자가 로컬 Keycloak 에 고객사를 흉내 낸 직원 8명과 그룹 9개를 만들어 Admin REST API 로 idp_members 를 내보내고, 접근 정책·MCP 승인 목록·도구 사전 세 표와 MCP tools/call 모양을 따른 도구 호출 16건을 넣습니다. 보안 엔지니어는 PoC 결과 화면에서 다섯 판정 묶음을 보고, 줄을 펼쳐 시킨 직원의 그룹 경로와 경로를 푼 자원, 같은 세션의 앞선 호출을 읽습니다. 판정 엔진은 Python(FastAPI)이고 Docker Compose 로 한 번에 뜹니다.
Claude Code 와 MCP 서버 셋(filesystem·sqlite·fetch) 사이에 Python 수집 프록시를 끼워 tools/call 요청을 tool_calls 와 같은 모양으로 남기고, 위험 호출 시나리오 24개(권한 밖 읽기·경로 우회·승인 안 된 서버·읽은 뒤 외부 전송 등)를 실제로 일으켜 시나리오 시험 탭에서 잡힌 호출과 놓친 호출을 봅니다. 판정은 JSON Lines 로도 내보내 고객 SIEM 이 받을 수 있는 모양을 갖춥니다.
Keycloak 에서 사용자를 비활성화하면 Admin REST API 로 당겨 idp_members 의 deactivated_at 을 채우고, 그 뒤 호출을 IdP 에서 빠진 사용자로 판정합니다. tool_catalog 에 없는 도구(export_csv 등)는 Claude Sonnet 4.5 가 MCP 설명을 읽고 종류를 제안하며, 보안 엔지니어가 승인한 제안만 사전에 들어가 다음 판정부터 쓰입니다.
보안 엔지니어가 PoC 결과 화면을 열면 인턴 김지훈(eng-intern)의 에이전트가 hr-db 서버 계정 svc-hr 로 성공시킨 hr/salary 조회(call-20931)가 권한 밖 묶음에 서고, 줄을 펼치면 eng-intern 이 허용하는 경로 repo/·wiki/·drive/eng/ 셋 옆에 자원이 놓입니다.
CISO 가 읽은 뒤 외부 전송 묶음을 열면 이민재의 세션 s-884 에서 secret/prod/db-password 를 읽은 지 180초 뒤 paste.example.net 으로 나간 http_post(call-21204)와, 정하나가 hr/ 를 읽고 정확히 600초 뒤 Slack 에 보낸 호출(call-21331)이 서고, 630초 뒤에 보낸 call-21362 는 허용된 호출에 남습니다.
대소문자만 다른 JH.Kim@Corp.example 호출(call-20945)은 IdP 구성원과 짝지어져 허용되고, drive/eng/../hr/ 로 돌아 들어간 호출(call-20960)은 경로를 풀어 권한 밖에 서며, 9월 30일 비활성화된 최도원의 10월 12일 호출(call-22310)은 IdP 에서 빠진 사용자로 나옵니다.
필드가 빠진 호출 줄, called_at 형식이 틀린 줄, 승인 목록 엑셀의 열 이름이 바뀐 파일 등 일부러 깨뜨린 입력 30종을 넣어도 화면은 멈추지 않고, 판정에서 뺀 줄이 몇 번째 줄이고 왜 빠졌는지 목록 위에 띄웁니다.
위 수치는 작성자가 만든 더미데이터 자가채점이며, 실제 탐지율과 오탐률은 핀타 수집 서버가 고객 환경에서 모은 호출과 고객사의 IdP·접근 정책이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 호출 16건과 그 정답을 모두 제가 만들었으므로, 다섯 판정이 맞게 나왔다는 결과는 적어 둔 규칙을 코드가 그대로 지킨다는 데까지만 말합니다. 실제 고객 호출의 탐지율과 오탐률은 PoC 데이터가 있어야 셉니다.
호출 16건에는 식의 실수 하나가 판정 하나를 뒤집는 줄이 넷 있습니다. drive/eng/../hr/ 경로(call-20960)는 ../ 를 풀지 않으면 허용된 호출로, 대소문자가 다른 user_id(call-20945)는 원문 그대로 짝지으면 IdP 에서 빠진 사용자로, 민감 읽기 뒤 정확히 600초에 나간 외부 전송(call-21331)은 10분 창을 '미만'으로 쓰면 허용된 호출로, 비활성화 5분 전 호출(call-22311)은 시각이 아니라 날짜로 견주면 IdP 에서 빠진 사용자로 바뀝니다.
Slack 같은 사내 메신저로 보낸 호출도 도구 사전에서 외부 전송이면 민감 자원 판정에 걸려, 인사팀이 평가 요약을 팀 채널에 올린 일이 call-21331 처럼 잡힙니다. 이 호출을 오탐으로 볼지는 고객사가 정할 일이라, 실제 도입에서는 사내 목적지를 따로 두는 룰 조정을 PoC 첫 주에 고객과 정합니다.
도구 사전에 없는 도구(export_csv)는 외부 전송으로 세지 않아, 그 도구로 민감 자원을 내보내면 이 판정은 놓칩니다. 화면은 종류 미상 표시를 달아 보안 엔지니어가 그 호출을 따로 보게 하고, 3주차 종류 제안이 이 빈자리를 줄입니다.
자원 경로 체계(hr/·secret/prod/·drive/eng/)는 수집 서버가 MCP 서버별 대상을 경로로 바꿔 남긴다는 가정 위에 있습니다. 공고는 수집 서버 배치와 에이전트 트래픽 연동까지만 밝히므로, 핀타 수집 서버가 자원을 SQL 문이나 URL 원문으로 남긴다면 경로로 바꾸는 층을 먼저 만듭니다.
이 포트폴리오는 도구 호출 수집, IdP 그룹 짝짓기, 세션 단위 탐지 룰로 호출마다 근거를 남기는 판정 엔진을 보여줍니다. 침해 사고 대응이나 금융권 보안 규제 전문성은 주장하지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
핀타 수집 서버가 남기는 에이전트 도구 호출 기록의 모양을 따른 표입니다. 한 줄이 MCP tools/call 요청 하나이고, 시킨 직원(user_id), 에이전트, MCP 서버와 그 서버가 쓴 계정(server_account), 도구, 자원 경로, 호출 시각이 있습니다. 자원은 수집 서버가 MCP 서버별 대상을 경로로 바꿔 남긴다는 가정입니다. 대소문자가 IdP 와 다르게 남은 user_id(call-20945), drive/eng/../hr/ 처럼 돌아 들어간 경로(call-20960), 민감 경로를 읽고 정확히 600초 뒤와 630초 뒤에 보낸 호출 한 쌍, 비활성화 5분 전 호출(call-22311)이 섞여 있습니다.
| call_id | session_id | user_id | agent | mcp_server | server_account | tool_name | resource | called_at | status |
|---|---|---|---|---|---|---|---|---|---|
| call-22310 | s-902 | dw.choi@corp.example | codex | jira | svc-jira | create_issue | jira/FIN-221 | 2026-10-12T09:41:00+09:00 | ok |
| call-20931 | s-871 | jh.kim@corp.example | claude-code | hr-db | svc-hr | query_table | hr/salary | 2026-10-13T10:14:00+09:00 | ok |
| call-20932 | s-871 | jh.kim@corp.example | claude-code | github | svc-github | read_file | repo/payments/README.md | 2026-10-13T10:16:00+09:00 | ok |
| call-20945 | s-872 | JH.Kim@Corp.example | cursor | confluence-wiki | svc-wiki | read_file | wiki/onboarding/setup.md | 2026-10-13T11:02:00+09:00 | ok |
| call-20960 | s-873 | jh.kim@corp.example | claude-code | shared-drive | svc-drive | read_file | drive/eng/../hr/salary_2026.xlsx | 2026-10-13T11:30:00+09:00 | ok |
아래 JSON Schema 에 맞는 에이전트 도구 호출 줄을 더 만들어 주세요. {"type":"object","properties":{"call_id":{"type":"string"},"session_id":{"type":"string"},"user_id":{"type":"string"},"agent":{"type":"string","enum":["claude-code","cursor","codex"]},"mcp_server":{"type":"string"},"server_account":{"type":"string"},"tool_name":{"type":"string"},"resource":{"type":"string"},"called_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["ok","error"]}},"required":["call_id","session_id","user_id","agent","mcp_server","server_account","tool_name","resource","called_at","status"]} 쉬운 데이터만 만들지 말 것. 다음 경우를 의도적으로 섞으세요: (1) MCP 서버 계정으로는 성공했지만 시킨 직원의 그룹 경로 밖인 자원, (2) 허용 경로로 시작하지만 ../ 로 다른 경로에 들어가는 resource, (3) 같은 session_id 에서 민감 경로를 읽고 정확히 600초 뒤와 601초 뒤에 외부 전송 도구를 부른 줄, (4) user_id 의 대소문자만 IdP 와 다른 줄, (5) 승인 목록에 없는 mcp_server 와 status 가 error 인 줄. called_at 은 +09:00 시간대를 붙여 주세요. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"call_id":{"type":"string"},"session_id":{"type":"string"},"user_id":{"type":"string"},"agent":{"type":"string","enum":["claude-code","cursor","codex"]},"mcp_server":{"type":"string"},"server_account":{"type":"string"},"tool_name":{"type":"string"},"resource":{"type":"string"},"called_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["ok","error"]}},"required":["call_id","session_id","user_id","agent","mcp_server","server_account","tool_name","resource","called_at","status"]}고객사 IdP 의 구성원·그룹 내보내기입니다. 1주차에는 지원자가 로컬 Keycloak 에 고객사를 흉내 낸 직원 8명과 그룹 9개를 만들고 Admin REST API 로 내보낸 값을 씁니다. 한 직원이 그룹 여럿에 들면 줄도 여럿이고(박서연·이민재), 퇴사나 계약 종료로 비활성화된 직원은 deactivated_at 에 그 시각이 있습니다.
| user_id | display_name | group | deactivated_at |
|---|---|---|---|
| jh.kim@corp.example | 김지훈 | eng-intern | — |
| sy.park@corp.example | 박서연 | eng-backend | — |
| sy.park@corp.example | 박서연 | data-analyst | — |
| mj.lee@corp.example | 이민재 | eng-platform | — |
| mj.lee@corp.example | 이민재 | secrets-admin | — |
아래 JSON Schema 에 맞는 IdP 구성원 줄을 더 만들어 주세요. {"type":"object","properties":{"user_id":{"type":"string"},"display_name":{"type":"string"},"group":{"type":"string"},"deactivated_at":{"type":["string","null"],"format":"date-time"}},"required":["user_id","display_name","group","deactivated_at"]} 쉬운 데이터만 만들지 말 것. 다음 경우를 의도적으로 섞으세요: (1) 그룹이 둘 이상인 직원, (2) deactivated_at 이 있는데 그 뒤에도 도구 호출이 남는 직원, (3) deactivated_at 바로 몇 분 전에 호출이 있는 직원, (4) 어느 접근 정책에도 그룹이 없는 직원. user_id 는 소문자 이메일로 맞춰 주세요. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"user_id":{"type":"string"},"display_name":{"type":"string"},"group":{"type":"string"},"deactivated_at":{"type":["string","null"],"format":"date-time"}},"required":["user_id","display_name","group","deactivated_at"]}고객사 보안팀이 엑셀로 보내는 그룹별 허용 자원 경로입니다. 경로마다 민감 여부(sensitive)가 있고, 판정은 시킨 직원의 모든 그룹 경로를 합쳐 자원과 앞부분 일치로 견줍니다.
| group | resource_prefix | sensitive |
|---|---|---|
| eng-intern | repo/ | N |
| eng-intern | wiki/ | N |
| eng-intern | drive/eng/ | N |
| eng-backend | repo/ | N |
| data-analyst | db/analytics/ | N |
아래 JSON Schema 에 맞는 그룹별 허용 경로를 더 만들어 주세요. {"type":"object","properties":{"group":{"type":"string"},"resource_prefix":{"type":"string"},"sensitive":{"type":"string","enum":["Y","N"]}},"required":["group","resource_prefix","sensitive"]} 쉬운 데이터만 만들지 말 것. 다음 경우를 의도적으로 섞으세요: (1) 다른 그룹 경로의 하위 경로(repo/ 와 repo/infra/), (2) 같은 경로를 여러 그룹이 공유하는 줄, (3) sensitive 가 Y 인 경로와 그 경로를 읽을 수 있는 그룹이 하나뿐인 경우. resource_prefix 는 / 로 끝내 주세요. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"group":{"type":"string"},"resource_prefix":{"type":"string"},"sensitive":{"type":"string","enum":["Y","N"]}},"required":["group","resource_prefix","sensitive"]}고객사가 승인한 MCP 서버 목록입니다. 목록에 없는 서버(local-files)와, 목록에는 있지만 승인 보류(approved N)인 서버(notion-sync)가 모두 등록 안 된 서버로 판정됩니다.
| mcp_server | approved | owner_team |
|---|---|---|
| github | Y | 플랫폼팀 |
| jira | Y | 플랫폼팀 |
| confluence-wiki | Y | 플랫폼팀 |
| hr-db | Y | 인사팀 |
| analytics-db | Y | 데이터팀 |
아래 JSON Schema 에 맞는 MCP 서버 승인 목록 줄을 더 만들어 주세요. {"type":"object","properties":{"mcp_server":{"type":"string"},"approved":{"type":"string","enum":["Y","N"]},"owner_team":{"type":"string"}},"required":["mcp_server","approved","owner_team"]} 쉬운 데이터만 만들지 말 것. 다음 경우를 의도적으로 섞으세요: (1) approved 가 N 인 서버, (2) 개발자가 노트북에 직접 붙여 목록에 없는 서버 이름을 tool_calls 쪽에 남길 수 있게 비슷한 이름의 승인 서버, (3) owner_team 이 보안팀인 서버. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"mcp_server":{"type":"string"},"approved":{"type":"string","enum":["Y","N"]},"owner_team":{"type":"string"}},"required":["mcp_server","approved","owner_team"]}도구 이름마다 종류(읽기·쓰기·외부 전송·실행)를 적은 탐지 룰 사전입니다. 민감 자원 판정은 이 표에서 외부 전송인 도구만 셉니다. 호출 기록의 export_csv 는 이 표에 없어 종류 미상으로 남고, 3주차 종류 제안의 첫 대상이 됩니다.
| tool_name | kind | description |
|---|---|---|
| read_file | 읽기 | Read the contents of a file at the giv… |
| query_table | 읽기 | Run a read-only SELECT against a table. |
| read_secret | 읽기 | Return the value stored at a secret pa… |
| search_issues | 읽기 | Search issues with a JQL query. |
| create_issue | 쓰기 | Create an issue in the given project. |
아래 JSON Schema 에 맞는 도구 사전 줄을 더 만들어 주세요. {"type":"object","properties":{"tool_name":{"type":"string"},"kind":{"type":"string","enum":["읽기","쓰기","외부 전송","실행"]},"description":{"type":"string"}},"required":["tool_name","kind","description"]} 쉬운 데이터만 만들지 말 것. 다음 경우를 의도적으로 섞으세요: (1) 이름만으로는 외부 전송인지 알 수 없는 도구(sync_*, export_*), (2) description 에 URL·destination 인자가 있는 외부 전송 도구, (3) 읽기처럼 보이지만 실행인 도구. description 은 MCP tools/list 응답처럼 영어 한 문장으로 써 주세요. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"tool_name":{"type":"string"},"kind":{"type":"string","enum":["읽기","쓰기","외부 전송","실행"]},"description":{"type":"string"}},"required":["tool_name","kind","description"]}| Python | 판정 엔진과 결과 API(FastAPI), 수집 프록시, Keycloak 내보내기 스크립트를 씁니다. |
| MCP 서버 | 2주차에 Claude Code 와 MCP 서버 사이에 끼우는 수집 프록시를 짜 tools/call 요청을 tool_calls 와 같은 모양으로 남기고, 3주차에는 tools/list 응답의 description·inputSchema 를 도구 종류 제안의 입력으로 씁니다. |
| AI 에이전트 · 코딩 에이전트 | Claude Code 를 실제로 돌려 위험 호출 시나리오 24개를 일으키고 판정이 잡는지 봅니다. |
| 런타임 텔레메트리 | 호출마다 세션·시킨 직원·서버 계정·호출 시각을 남기고, 같은 세션의 민감 읽기와 외부 전송을 10분 창으로 잇는 세션 탐지를 이 기록 위에서 돌립니다. |
| 고객사 IdP (Keycloak · OIDC) | 로컬 Keycloak 에 직원과 그룹을 만들고 Admin REST API 로 idp_members 를 내보냅니다. 3주차에는 사용자 비활성화를 바로 당겨 다음 판정에 반영합니다. |
| 탐지 룰 | 다섯 판정을 우선순위가 있는 룰 목록으로 두고, 10분 창·민감 경로·외부 전송 도구를 고객별 설정 파일에서 바꿉니다. |
| SIEM | 2주차에 판정을 JSON Lines 로 내보내 고객 SIEM 이 그대로 받게 합니다. |
| Docker Compose | 수집 프록시·판정 API·결과 화면을 한 번에 띄워 고객 환경에 수집 서버를 두는 모양을 재현합니다. |
| Claude Sonnet 4.5 | 3주차에 사전에 없는 도구의 종류를 MCP 설명에서 제안합니다. |
✗ fail → 다섯 호출 중 하나라도 반대로 서면 resource 를 풀기 전에 견주지 않았는지, 10분 창을 '미만'으로 쓰지 않았는지, user_id 를 원문 그대로 짝짓지 않았는지부터 봅니다.
✓ pass → 여기서 멈춰도 PoC 결과 회의에 들고 갈 판정 화면이 완성됩니다. 더 키우려면 직접 돌린 에이전트의 실제 호출을 모아 시나리오 24개로 시험하는 단계로 갑니다.
✗ fail → 놓친 호출이 외부 전송 규칙에 몰리면 프록시가 남긴 tool_name 이 사전에 있는지부터 보고, 10분 창 값은 그대로 둡니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 Keycloak 비활성화를 바로 반영하고 사전에 없는 도구의 종류 제안을 붙입니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · call_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
허용된 호출허용시킨 직원의 권한 밖 자원권한 밖등록 안 된 MCP 서버미등록 서버IdP 에서 빠진 사용자IdP 이탈민감 자원을 읽고 바로 외부로 보냄읽은 뒤 외부 전송도구 호출 판정입니다. 에이전트의 도구 호출 한 건이 다섯 판정 중 어디에 서는지와, 판정에 쓴 시킨 직원의 그룹, 경로를 푼 자원, 맞은 허용 경로, 도구 종류, 같은 세션의 앞선 민감 읽기와 그 뒤 경과 초를 남기는 형식입니다. finding 은 판정마다 정해 둔 문형이라 모델이 쓰지 않습니다.
{"type":"object","properties":{"call_id":{"type":"string","description":"판정 대상 호출입니다. tool_calls.call_id 값입니다."},"session_id":{"type":"string"},"user_id":{"type":"string","description":"tool_calls.user_id 원문입니다."},"mcp_server":{"type":"string"},"tool_name":{"type":"string"},"resource":{"type":"string"},"called_at":{"type":"string","format":"date-time"},"verdict":{"type":"string","enum":["허용된 호출","시킨 직원의 권한 밖 자원","등록 안 된 MCP 서버","IdP 에서 빠진 사용자","민감 자원을 읽고 바로 외부로 보냄"],"description":"조건이 여럿 성립하면 IdP 에서 빠진 사용자, 등록 안 된 MCP 서버, 민감 자원을 읽고 바로 외부로 보냄, 시킨 직원의 권한 밖 자원 순으로 하나를 고릅니다."},"also_flagged":{"type":"array","items":{"type":"string","enum":["시킨 직원의 권한 밖 자원","등록 안 된 MCP 서버","IdP 에서 빠진 사용자","민감 자원을 읽고 바로 외부로 보냄"]},"description":"verdict 로 고르지 않았지만 조건이 함께 성립한 판정입니다. IdP 에서 빠진 사용자로 판정된 호출은 그룹을 쓰지 않으므로 권한 밖을 달지 않습니다."},"groups":{"type":"array","items":{"type":"string"},"description":"user_id 를 소문자로 맞춰 idp_members 에서 얻은 group 전부입니다. IdP 에서 빠진 사용자면 빈 배열입니다."},"resolved_resource":{"type":"string","description":"resource 의 ./ 와 ../ 를 풀어 얻은 경로입니다. 앞부분 일치는 이 값으로 견줍니다."},"matched_prefix":{"type":["string","null"],"description":"groups 의 access_policy.resource_prefix 중 resolved_resource 가 그것으로 시작하는 가장 긴 경로입니다. 없으면 null 입니다."},"tool_kind":{"type":["string","null"],"description":"tool_catalog.kind 값입니다. 사전에 없는 도구면 null 입니다."},"prior_sensitive_call_id":{"type":["string","null"],"description":"외부 전송 호출에만 채웁니다. 같은 session_id 에서 이 호출보다 앞서 sensitive 가 Y 인 resource_prefix 로 시작하는 자원을 읽기 도구로 읽은 가장 늦은 호출의 call_id 입니다."},"seconds_since_read":{"type":["integer","null"],"minimum":0,"description":"이 호출의 called_at − prior_sensitive_call_id 호출의 called_at 을 초로 계산한 값입니다. 두 시각은 시간대를 붙여 시각으로 바꾼 뒤 뺍니다."},"evidence":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string"},"field":{"type":"string"},"agg":{"type":"string","enum":["count","sum","avg","min","max"]},"value":{"type":["string","integer","number","boolean","null"]}},"required":["field","value"]}},"finding":{"type":"string","description":"판정마다 정해 둔 문형에 호출 값을 채운 한 문장입니다. 모델이 쓰지 않습니다."},"hedge":{"type":["string","null"]}},"required":["call_id","session_id","user_id","mcp_server","tool_name","resource","called_at","verdict","also_flagged","groups","resolved_resource","matched_prefix","tool_kind","prior_sensitive_call_id","seconds_since_read","evidence","finding","hedge"]}[
{
"call_id": "call-20931",
"session_id": "s-871",
"user_id": "jh.kim@corp.example",
"mcp_server": "hr-db",
"tool_name": "query_table",
"resource": "hr/salary",
"called_at": "2026-10-13T10:14:00+09:00",
"verdict": "시킨 직원의 권한 밖 자원",
"also_flagged": [],
"groups": [
"eng-intern"
],
"resolved_resource": "hr/salary",
"matched_prefix": null,
"tool_kind": "읽기",
"prior_sensitive_call_id": null,
"seconds_since_read": null,
"evidence": [
{
"field": "call_id",
"value": "call-20931"
},
{
"field": "server_account",
"value": "svc-hr"
},
{
"scope": "시킨 직원",
"field": "group",
"value": "eng-intern"
},
{
"scope": "eng-intern 허용 경로",
"field": "resource_prefix",
"value": "repo/"
},
{
"scope": "eng-intern 허용 경로",
"field": "resource_prefix",
"value": "wiki/"
},
{
"scope": "eng-intern 허용 경로",
"field": "resource_prefix",
"value": "drive/eng/"
},
{
"scope": "자원과 앞부분이 맞는 eng-intern 경로",
"field": "resource_prefix",
"agg": "count",
"value": 0
},
{
"field": "resource",
"value": "hr/salary"
}
],
"finding": "인턴 김지훈이 시킨 query_table 이 MCP 서버 계정 svc-hr 로 hr/salary 를 읽었고, eng-intern 그룹이 허용하는 경로 셋 중 맞는 경로가 없습니다.",
"hedge": "hr/salary 는 민감 경로(hr/)지만 같은 세션에서 10분 안에 외부 전송 호출이 없어 권한 밖 판정만 답니다."
},
{
"call_id": "call-20960",
"session_id": "s-873",
"user_id": "jh.kim@corp.example",
"mcp_server": "shared-drive",
"tool_name": "read_file",
"resource": "drive/eng/../hr/salary_2026.xlsx",
"called_at": "2026-10-13T11:30:00+09:00",
"verdict": "시킨 직원의 권한 밖 자원",
"also_flagged": [],
"groups": [
"eng-intern"
],
"resolved_resource": "drive/hr/salary_2026.xlsx",
"matched_prefix": null,
"tool_kind": "읽기",
"prior_sensitive_call_id": null,
"seconds_since_read": null,
"evidence": [
{
"field": "call_id",
"value": "call-20960"
},
{
"field": "resource",
"value": "drive/eng/../hr/salary_2026.xlsx"
},
{
"scope": "시킨 직원",
"field": "group",
"value": "eng-intern"
},
{
"scope": "eng-intern 허용 경로",
"field": "resource_prefix",
"value": "drive/eng/"
},
{
"scope": "경로를 푼 자원과 앞부분이 맞는 eng-intern 경로",
"field": "resource_prefix",
"agg": "count",
"value": 0
}
],
"finding": "김지훈이 시킨 read_file 이 drive/eng/ 를 거쳐 drive/hr/salary_2026.xlsx 를 읽었고, 경로를 풀면 eng-intern 이 허용하는 경로 어디에도 들지 않습니다.",
"hedge": "원문 resource 는 drive/eng/ 로 시작해 경로를 풀지 않고 앞부분만 견주면 허용된 호출로 잘못 섭니다."
},
{
"call_id": "call-21107",
"session_id": "s-880",
"user_id": "sy.park@corp.example",
"mcp_server": "local-files",
"tool_name": "read_file",
"resource": "~/.aws/credentials",
"called_at": "2026-10-13T13:40:00+09:00",
"verdict": "등록 안 된 MCP 서버",
"also_flagged": [
"시킨 직원의 권한 밖 자원"
],
"groups": [
"eng-backend",
"data-analyst"
],
"resolved_resource": "~/.aws/credentials",
"matched_prefix": null,
"tool_kind": "읽기",
"prior_sensitive_call_id": null,
"seconds_since_read": null,
"evidence": [
{
"field": "call_id",
"value": "call-21107"
},
{
"field": "mcp_server",
"value": "local-files"
},
{
"scope": "mcp_inventory 의 local-files",
"field": "mcp_server",
"agg": "count",
"value": 0
},
{
"scope": "시킨 직원",
"field": "group",
"value": "eng-backend"
},
{
"scope": "시킨 직원",
"field": "group",
"value": "data-analyst"
},
{
"scope": "자원과 앞부분이 맞는 두 그룹 경로",
"field": "resource_prefix",
"agg": "count",
"value": 0
},
{
"field": "server_account",
"value": "local:sy.park"
}
],
"finding": "박서연이 시킨 read_file 이 승인 목록에 없는 MCP 서버 local-files 로 ~/.aws/credentials 를 읽었습니다.",
"hedge": "server_account 가 개인 계정(local:sy.park)이라 개발자 노트북에 직접 붙인 서버로 보입니다. 승인 목록에 올릴지는 고객사 보안팀이 정합니다."
},
{
"call_id": "call-21204",
"session_id": "s-884",
"user_id": "mj.lee@corp.example",
"mcp_server": "web-fetch",
"tool_name": "http_post",
"resource": "https://paste.example.net/u/8f2a",
"called_at": "2026-10-13T14:05:00+09:00",
"verdict": "민감 자원을 읽고 바로 외부로 보냄",
"also_flagged": [
"시킨 직원의 권한 밖 자원"
],
"groups": [
"eng-platform",
"secrets-admin"
],
"resolved_resource": "https://paste.example.net/u/8f2a",
"matched_prefix": null,
"tool_kind": "외부 전송",
"prior_sensitive_call_id": "call-21201",
"seconds_since_read": 180,
"evidence": [
{
"field": "call_id",
"value": "call-21204"
},
{
"field": "kind",
"value": "외부 전송"
},
{
"scope": "앞선 민감 읽기",
"field": "call_id",
"value": "call-21201"
},
{
"scope": "앞선 민감 읽기",
"field": "resource",
"value": "secret/prod/db-password"
},
{
"scope": "secret/prod/",
"field": "sensitive",
"value": "Y"
},
{
"scope": "앞선 민감 읽기",
"field": "called_at",
"value": "2026-10-13T14:02:00+09:00"
},
{
"scope": "이 호출",
"field": "called_at",
"value": "2026-10-13T14:05:00+09:00"
}
],
"finding": "이민재의 세션 s-884 에서 secret/prod/db-password 를 읽은 지 180초 뒤 외부 전송 도구 http_post 가 paste.example.net 으로 나갔습니다.",
"hedge": "앞선 읽기 호출 call-21201 은 secrets-admin 그룹 경로 안이라 허용된 호출이고, 판정은 뒤의 외부 전송 호출에 답니다."
},
{
"call_id": "call-21331",
"session_id": "s-890",
"user_id": "hn.jung@corp.example",
"mcp_server": "slack",
"tool_name": "post_message",
"resource": "slack/#hr-ops",
"called_at": "2026-10-13T15:30:00+09:00",
"verdict": "민감 자원을 읽고 바로 외부로 보냄",
"also_flagged": [],
"groups": [
"hr"
],
"resolved_resource": "slack/#hr-ops",
"matched_prefix": "slack/",
"tool_kind": "외부 전송",
"prior_sensitive_call_id": "call-21330",
"seconds_since_read": 600,
"evidence": [
{
"field": "call_id",
"value": "call-21331"
},
{
"field": "kind",
"value": "외부 전송"
},
{
"scope": "앞선 민감 읽기",
"field": "call_id",
"value": "call-21330"
},
{
"scope": "앞선 민감 읽기",
"field": "resource",
"value": "hr/evaluations_2026h1"
},
{
"scope": "hr/",
"field": "sensitive",
"value": "Y"
},
{
"scope": "앞선 민감 읽기",
"field": "called_at",
"value": "2026-10-13T15:20:00+09:00"
},
{
"scope": "이 호출",
"field": "called_at",
"value": "2026-10-13T15:30:00+09:00"
}
],
"finding": "정하나의 세션 s-890 에서 hr/evaluations_2026h1 을 읽은 지 정확히 600초 뒤 post_message 가 slack/#hr-ops 로 나갔습니다.",
"hedge": "10분 창은 600초를 포함합니다. 사내 채널로 보낸 호출이라, 고객사가 Slack 을 사내 목적지로 정하면 이 호출은 허용된 호출로 바뀝니다."
},
{
"call_id": "call-21362",
"session_id": "s-891",
"user_id": "hn.jung@corp.example",
"mcp_server": "slack",
"tool_name": "post_message",
"resource": "slack/#hr-ops",
"called_at": "2026-10-13T16:10:30+09:00",
"verdict": "허용된 호출",
"also_flagged": [],
"groups": [
"hr"
],
"resolved_resource": "slack/#hr-ops",
"matched_prefix": "slack/",
"tool_kind": "외부 전송",
"prior_sensitive_call_id": "call-21360",
"seconds_since_read": 630,
"evidence": [
{
"field": "call_id",
"value": "call-21362"
},
{
"field": "kind",
"value": "외부 전송"
},
{
"scope": "앞선 민감 읽기",
"field": "call_id",
"value": "call-21360"
},
{
"scope": "앞선 민감 읽기",
"field": "called_at",
"value": "2026-10-13T16:00:00+09:00"
},
{
"scope": "이 호출",
"field": "called_at",
"value": "2026-10-13T16:10:30+09:00"
},
{
"scope": "시킨 직원",
"field": "group",
"value": "hr"
},
{
"scope": "hr 허용 경로",
"field": "resource_prefix",
"value": "slack/"
}
],
"finding": "정하나가 시킨 post_message 는 hr 그룹 경로 slack/ 안이고, 같은 세션의 민감 읽기가 630초 전이라 10분 창 밖입니다.",
"hedge": null
},
{
"call_id": "call-22310",
"session_id": "s-902",
"user_id": "dw.choi@corp.example",
"mcp_server": "jira",
"tool_name": "create_issue",
"resource": "jira/FIN-221",
"called_at": "2026-10-12T09:41:00+09:00",
"verdict": "IdP 에서 빠진 사용자",
"also_flagged": [],
"groups": [],
"resolved_resource": "jira/FIN-221",
"matched_prefix": null,
"tool_kind": "쓰기",
"prior_sensitive_call_id": null,
"seconds_since_read": null,
"evidence": [
{
"field": "call_id",
"value": "call-22310"
},
{
"field": "user_id",
"value": "dw.choi@corp.example"
},
{
"field": "deactivated_at",
"value": "2026-09-30T18:00:00+09:00"
},
{
"field": "called_at",
"value": "2026-10-12T09:41:00+09:00"
}
],
"finding": "9월 30일에 비활성화된 최도원의 user_id 로 10월 12일 create_issue 가 jira 에 나갔습니다.",
"hedge": "에이전트가 비활성화 전에 받은 토큰을 계속 쓴 것으로 보입니다. 토큰이 어디에 남았는지는 고객사가 확인합니다."
},
{
"call_id": "call-20945",
"session_id": "s-872",
"user_id": "JH.Kim@Corp.example",
"mcp_server": "confluence-wiki",
"tool_name": "read_file",
"resource": "wiki/onboarding/setup.md",
"called_at": "2026-10-13T11:02:00+09:00",
"verdict": "허용된 호출",
"also_flagged": [],
"groups": [
"eng-intern"
],
"resolved_resource": "wiki/onboarding/setup.md",
"matched_prefix": "wiki/",
"tool_kind": "읽기",
"prior_sensitive_call_id": null,
"seconds_since_read": null,
"evidence": [
{
"field": "call_id",
"value": "call-20945"
},
{
"field": "user_id",
"value": "JH.Kim@Corp.example"
},
{
"scope": "IdP 구성원",
"field": "user_id",
"value": "jh.kim@corp.example"
},
{
"scope": "시킨 직원",
"field": "group",
"value": "eng-intern"
},
{
"scope": "eng-intern 허용 경로",
"field": "resource_prefix",
"value": "wiki/"
}
],
"finding": "user_id 의 대소문자를 맞추면 김지훈과 짝지어지고, wiki/onboarding/setup.md 는 eng-intern 경로 wiki/ 안입니다.",
"hedge": "이 세션의 user_id 는 IdP 와 대소문자가 다르게 기록돼 있어, 원문 그대로 짝지으면 IdP 에서 빠진 사용자로 잘못 섭니다."
}
][자동 검증 · 코드가 다섯 표에서 다시 셉니다] 1. verdict 가 enum 다섯 값 중 하나이고, also_flagged 의 각 값이 허용된 호출을 뺀 네 값 중 하나인지 확인합니다. 2. call_id·verdict·groups·resolved_resource·seconds_since_read·evidence·finding 등 required 필드가 모두 있는지 확인합니다. 3. evidence 의 field 가 다섯 표의 컬럼(call_id·user_id·mcp_server·server_account·resource·called_at·group·deactivated_at·resource_prefix·sensitive·kind)에 실재하는 이름인지 대조합니다. 4. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 샘플 행의 값 그대로인지 대조합니다. 5. seconds_since_read = 이 호출의 called_at − prior_sensitive_call_id 호출의 called_at 을 초로 다시 계산합니다. call-21204 는 180, call-21331 은 600, call-21362 는 630 입니다. 6. user_id 를 소문자로 맞춰도 idp_members 에 같은 user_id 가 없거나, 있는데 called_at 이 deactivated_at 보다 늦으면 verdict 는 IdP 에서 빠진 사용자입니다. 성립 사례는 call-22310(9월 30일 비활성화 뒤 10월 12일 호출), 불성립 사례는 대소문자만 다른 call-20945 입니다. 7. 위에 걸리지 않고 mcp_server 가 mcp_inventory 에 없거나 approved 가 N 이면 verdict 는 등록 안 된 MCP 서버입니다. 성립 사례는 call-21107(local-files), 불성립 사례는 call-20931(hr-db 는 승인 Y)입니다. 8. 위 둘에 걸리지 않고 tool_kind 가 외부 전송이며 seconds_since_read 가 600 이하이면 verdict 는 민감 자원을 읽고 바로 외부로 보냄입니다. 성립 사례는 call-21204(180초)와 call-21331(정확히 600초), 불성립 사례는 call-21362(630초)입니다. 9. 위 셋에 걸리지 않고 resolved_resource 가 groups 의 resource_prefix 어느 것으로도 시작하지 않으면 verdict 는 시킨 직원의 권한 밖 자원이고, 시작하면 허용된 호출입니다. 성립 사례는 call-20931 과 경로를 풀어야 드러나는 call-20960, 불성립 사례는 call-21362(slack/ 안)입니다. 10. also_flagged 는 verdict 로 고르지 않은 판정 중 조건이 성립한 값입니다. 성립 사례는 call-21107·call-21204(둘 다 권한 밖이 함께 성립), 불성립 사례는 call-21331 입니다. 11. finding 이 존댓말 한 문장으로 끝나는지 확인합니다. [수동 검증 · 사람이 봅니다] 1. 보안 엔지니어가 펼친 줄의 그룹 경로와 세션 흐름만 보고 CISO 에게 탐지 근거를 설명하는지, PoC 결과 회의 한 번으로 봅니다. 2. 사내 채널로 보낸 call-21331 같은 호출을 고객사가 오탐으로 보는지 듣고, 그 답을 탐지 룰 설정에 옮깁니다. [평가 러너 · 2주차] 1. 수집 프록시 뒤에서 Claude Code 로 위험 호출 시나리오 24개를 실제로 일으키고, 시나리오가 의도한 판정과 같은 판정이 난 호출 수를 셉니다. 2. 놓친 호출은 어느 규칙(IdP 짝짓기·승인 목록·세션 10분 창·그룹 경로)에서 갈렸는지 같은 줄에 남깁니다.
근거: 실행 맥락을 바탕으로 위험한 행동을 탐지·차단하는 AI 보안 제품을 만들고 있습니다.