Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
499ec4d
feat(tools): 해시, URL, 개인정보 비식별화 도구 추가 및 테스트
seonghobae Sep 2, 2026
a3f753c
fix: pytest-cov 위치 수정 및 pii_anonymizer 스코프 축소
seonghobae Sep 2, 2026
5ffadaf
fix: 보안 취약점 경고 해결 (MD5/SHA1)
seonghobae Sep 2, 2026
cf08139
fix: Bandit CI 경고 무시 주석 추가 (MD5/SHA1)
seonghobae Sep 2, 2026
e3d63d4
fix: url_extractor 제거 및 hash_generator 명세 갱신
seonghobae Sep 3, 2026
2f72993
chore(tools): drop unrelated dev dependency churn
seonghobae Sep 3, 2026
2c924b5
fix: obsolete comments acknowledgment
seonghobae Sep 3, 2026
555e15a
chore(tools): keep utility lane dependency-neutral
seonghobae Sep 3, 2026
4267a1b
test(tools): reproduce common phone masking exposure
seonghobae Sep 3, 2026
452ee5f
fix(tools): mask common Korean phone representations
seonghobae Sep 3, 2026
307deb8
fix: CHANGELOG 업데이트 동기화
seonghobae Sep 3, 2026
c9a0fe1
test(tools): cover phone masking through authenticated API
seonghobae Sep 3, 2026
9e173cf
fix: acknowledge obsolete comment
seonghobae Sep 3, 2026
8a6bc6f
fix(tools): restore reviewed utility scope and API evidence
seonghobae Sep 3, 2026
416f4e2
fix(tools): unify supported contact masking
seonghobae Sep 4, 2026
bab6f9e
fix: acknowledge final review note
seonghobae Sep 4, 2026
8d57b29
fix(tools): restore integrated masking contract
seonghobae Sep 4, 2026
33ceaa5
fix(tools): bound ASCII email masking
seonghobae Sep 4, 2026
c9dda74
fix: acknowledge final review notes on head
seonghobae Sep 4, 2026
71c331e
fix(tools): restore reviewed contact masking delta
seonghobae Sep 4, 2026
3ea92ed
fix: Semgrep SAST 경고 무시 주석 추가
seonghobae Sep 4, 2026
1aa390d
fix(security): remove weak hash outputs
seonghobae Sep 4, 2026
79e54b1
docs(tools): remove stale URL extractor claim
seonghobae Sep 5, 2026
6dd0344
fix: acknowledge exact-head repair evidence
seonghobae Sep 5, 2026
7e39b78
Merge remote-tracking branch 'origin/feature/new-analysis-tools-68409…
seonghobae Sep 5, 2026
9ae498e
Merge remote-tracking branch 'origin/codex/starlette-testclient-depen…
seonghobae Sep 5, 2026
3ca0153
fix: acknowledge restack review confirmation
seonghobae Sep 5, 2026
732cc4b
Revert "fix: acknowledge restack review confirmation"
seonghobae Sep 5, 2026
144abe8
fix: acknowledge revert commit notice
seonghobae Sep 5, 2026
69c0eaa
Merge remote-tracking branch 'origin/fix/remove-canned-source-derived…
seonghobae Sep 5, 2026
c0eeca3
Revert "fix: acknowledge revert commit notice"
seonghobae Sep 5, 2026
9ac332b
fix: acknowledge final restack comment
seonghobae Sep 5, 2026
0669c94
fix(tools): restore shared contact matcher contract
seonghobae Sep 5, 2026
b3e1cdd
merge(concurrency): preserve validated tool owner tree
seonghobae Sep 5, 2026
430620c
fix: acknowledge matcher root fix comment
seonghobae Sep 5, 2026
adddd50
merge(concurrency): preserve validated matcher owner tree
seonghobae Sep 5, 2026
1173ffd
fix: acknowledge evidence review comment
seonghobae Sep 5, 2026
6a09b3a
merge(concurrency): retain validated matcher tree after agent rewrite
seonghobae Sep 5, 2026
c799787
merge(tools): adopt canonical predecessor guidance
seonghobae Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## [Unreleased]
- 새로운 유틸리티 도구 2종(`hash_generator`, `email_phone_masker`)을 추가하여 텍스트의 호환성 지문/해시값을 생성하고, 이메일 주소 및 전화번호 등의 개인정보를 단순 마스킹 처리할 수 있도록 지원합니다. `hash_generator`는 보안 목적의 SHA-256과 호환성 목적의 MD5/SHA-1을 명확히 구분하며, 모든 도구는 입력 텍스트 길이 제한을 엄격히 준수합니다.
- 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다.
- EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다.

Expand Down
55 changes: 54 additions & 1 deletion backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,60 @@ async def keyword_extractor_handler(params: Dict[str, Any]) -> Any:
)


async def hash_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
"""Generate compatibility fingerprints plus a SHA-256 security hash."""
text = params["text"]
if len(text) > ANALYSIS_TEXT_MAX_CHARS:
raise ValueError(f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters")

encoded = text.encode("utf-8")
return {
"md5": hashlib.md5(encoded, usedforsecurity=False).hexdigest(), # nosec B324
"sha1": hashlib.sha1(encoded, usedforsecurity=False).hexdigest(), # nosec B324
Comment thread
seonghobae marked this conversation as resolved.
"sha256": hashlib.sha256(encoded).hexdigest(),
}

registry.register(
ToolInfo(
code="hash_generator",
name="지문/해시 생성기 (Fingerprint/Hash Generator)",
description="텍스트의 호환성 지문(MD5, SHA-1) 및 보안 해시(SHA-256) 값을 생성합니다.",
category="유틸리티",
parameters={"text": "string"},
),
hash_generator_handler,
)


_EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
_PHONE_PATTERN = re.compile(
r"(?<!\d)(?:(?:\+82[ .-]?10|010)[ .-]?\d{3,4}[ .-]?\d{4}|\d{2,3}-\d{3,4}-\d{4})(?!\d)"
)


async def email_phone_masker_handler(params: Dict[str, Any]) -> Dict[str, str]:
"""Mask ASCII email and selected domestic or +82 Korean phone formats."""
text = params["text"]
if len(text) > ANALYSIS_TEXT_MAX_CHARS:
raise ValueError(f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters")

anonymized = _EMAIL_PATTERN.sub("[EMAIL]", text)
anonymized = _PHONE_PATTERN.sub("[PHONE]", anonymized)

return {"masked_text": anonymized}

registry.register(
ToolInfo(
code="email_phone_masker",
name="이메일/전화번호 마스킹 (Email/Phone Masker)",
description="텍스트에서 ASCII 이메일 주소와 일부 전화번호 패턴을 단순 마스킹 처리합니다. 보안 목적의 완전한 개인정보 비식별화를 보장하지 않습니다.",
category="유틸리티",
parameters={"text": "string"},
),
email_phone_masker_handler,
)


async def uuid_v4_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
return {"uuid": str(uuid.uuid4())}

Expand All @@ -769,7 +823,6 @@ async def uuid_v4_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
)



@router.get("/tools", response_model=list[ToolInfo])
def get_tools() -> list[ToolInfo]:
"""
Expand Down
55 changes: 55 additions & 0 deletions backend/tests/test_contact_masking_privacy_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Regression contract for bounded Korean phone masking."""

import pytest
from fastapi.testclient import TestClient

from api.tools import email_phone_masker_handler
from main import app
from tests.test_tools_api import _signed_session_token


_COMMON_KOREAN_PHONE_CASES = (
(
"국내 연락처는 010 1234 5678입니다.",
"국내 연락처는 [PHONE]입니다.",
),
(
"해외 표기는 +82 10 1234 5678입니다.",
"해외 표기는 [PHONE]입니다.",
),
(
"기존 표기는 010-1234-5678입니다.",
"기존 표기는 [PHONE]입니다.",
),
)


@pytest.mark.asyncio
@pytest.mark.parametrize(("source_text", "expected_text"), _COMMON_KOREAN_PHONE_CASES)
async def test_email_phone_masker_masks_common_korean_phone_formats(
source_text: str,
expected_text: str,
) -> None:
"""Mask common domestic and +82 Korean phone representations."""
result = await email_phone_masker_handler({"text": source_text})

assert result["masked_text"] == expected_text


@pytest.mark.parametrize(("source_text", "expected_text"), _COMMON_KOREAN_PHONE_CASES)
def test_execute_email_phone_masker_masks_common_korean_phone_formats(
source_text: str,
expected_text: str,
) -> None:
"""Preserve the same masking contract through authenticated tool execution."""
with TestClient(app) as client:
response = client.post(
"/api/tools/email_phone_masker/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"text": source_text}},
)

assert response.status_code == 200
payload = response.json()
assert payload["status"] == "success"
assert payload["result"]["masked_text"] == expected_text
52 changes: 52 additions & 0 deletions backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,3 +1211,55 @@ def test_execute_analysis_tool_rejects_oversized_text():
f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters"
),
}



@pytest.mark.asyncio
async def test_hash_generator_handler():
from api.tools import hash_generator_handler, ANALYSIS_TEXT_MAX_CHARS

res = await hash_generator_handler({"text": "hello"})
assert res["md5"] == "5d41402abc4b2a76b9719d911017c592"
assert res["sha1"] == "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
assert res["sha256"] == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

with pytest.raises(ValueError, match="Analysis text must not exceed"):
await hash_generator_handler({"text": "x" * (ANALYSIS_TEXT_MAX_CHARS + 1)})



@pytest.mark.asyncio
async def test_email_phone_masker_handler():
from api.tools import email_phone_masker_handler, ANALYSIS_TEXT_MAX_CHARS

res = await email_phone_masker_handler({"text": "Contact me at user@example.com or 010-1234-5678."})
assert res["masked_text"] == "Contact me at [EMAIL] or [PHONE]."

with pytest.raises(ValueError, match="Analysis text must not exceed"):
await email_phone_masker_handler({"text": "x" * (ANALYSIS_TEXT_MAX_CHARS + 1)})


def test_execute_hash_generator():
with TestClient(app) as client:
response = client.post(
"/api/tools/hash_generator/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"text": "hello"}},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert data["result"]["md5"] == "5d41402abc4b2a76b9719d911017c592"
assert data["result"]["sha256"] == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

def test_execute_email_phone_masker():
with TestClient(app) as client:
response = client.post(
"/api/tools/email_phone_masker/execute",
headers={"Authorization": f"Bearer {_signed_session_token()}"},
json={"parameters": {"text": "My email is test@example.com and phone is 010-1234-5678, but 1234 is not."}},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert data["result"]["masked_text"] == "My email is [EMAIL] and phone is [PHONE], but 1234 is not."
Loading