Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
d1e445c
chore(tools): retire duplicate legacy fingerprint lane
seonghobae Sep 15, 2026
3f2df44
chore(tools): freeze predecessor tree pending descendant migration
seonghobae Sep 15, 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`)을 추가했습니다. 해시 도구는 MD5·SHA-1 호환 fingerprint와 SHA-256을 구분하고, 연락처 도구는 제한된 길이 안에서 이메일 주소와 전화번호를 단순 마스킹합니다.
### Source-bound 요약·업무·관계·일정 경계

- 입력과 무관한 고정 2023 fixture로 결정 사항과 미해결 질문, 업무와 마감일,
Expand Down
62 changes: 62 additions & 0 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,68 @@ 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
"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_ATOM = r"A-Za-z0-9!#$%&'*+/=?^_`{|}~"
_EMAIL_PATTERN = re.compile(
rf"(?<![{_EMAIL_ATOM}.-])"
rf"[{_EMAIL_ATOM}-]+(?:\.[{_EMAIL_ATOM}-]+)*@"
rf"(?:[A-Za-z0-9](?:[A-Za-z0-9-]{{0,61}}[A-Za-z0-9])?\.)+"
r"[A-Za-z]{2,63}(?![A-Za-z0-9-])"
)
_PHONE_PATTERN = re.compile(
r"(?<!\d)(?:(?:\+82[ .-]?10|010)[ .-]?\d{3,4}[ .-]?\d{4}"
r"|\d{2,3}-\d{3,4}-\d{4}"
r"|(?:\+?1[ .-]?)?(?:\(\d{3}\)|\d{3})[ .-]?\d{3}[ .-]?\d{4})(?!\d)"
)


async def email_phone_masker_handler(params: Dict[str, Any]) -> Dict[str, str]:
"""Mask ASCII email and selected Korean or North American 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]:
"""Generate one RFC 9562 UUID version 4 for the retained built-in utility."""
return {"uuid": str(uuid.uuid4())}
Expand Down
45 changes: 45 additions & 0 deletions backend/tests/test_contact_masking_privacy_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest

from api.tools import email_phone_masker_handler


@pytest.mark.asyncio
@pytest.mark.parametrize(
("source_text", "expected_text"),
[
(
"국내 연락처는 010 1234 5678입니다.",
"국내 연락처는 [PHONE]입니다.",
),
(
"해외 표기는 +82 10 1234 5678입니다.",
"해외 표기는 [PHONE]입니다.",
),
(
"기존 표기는 010-1234-5678입니다.",
"기존 표기는 [PHONE]입니다.",
),
(
"북미 연락처는 +1 (415) 555-2671입니다.",
"북미 연락처는 [PHONE]입니다.",
),
],
)
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.asyncio
async def test_email_phone_masker_rejects_malformed_email_domain() -> None:
"""Do not consume malformed addresses while masking valid neighbors."""
result = await email_phone_masker_handler(
{"text": "Keep a@b..com visible; mask support@example.com."}
)

assert result["masked_text"] == "Keep a@b..com visible; mask [EMAIL]."
52 changes: 52 additions & 0 deletions backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,3 +1080,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."