-
Notifications
You must be signed in to change notification settings - Fork 1
feat(tools): add compatibility fingerprints and bounded contact masking #1538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/remove-canned-source-derived-tools
Are you sure you want to change the base?
Changes from 18 commits
499ec4d
a3f753c
5ffadaf
cf08139
e3d63d4
2f72993
2c924b5
555e15a
4267a1b
452ee5f
307deb8
c9a0fe1
9e173cf
8a6bc6f
416f4e2
bab6f9e
8d57b29
33ceaa5
c9dda74
71c331e
3ea92ed
1aa390d
79e54b1
6dd0344
7e39b78
9ae498e
3ca0153
732cc4b
144abe8
69c0eaa
c0eeca3
9ac332b
0669c94
b3e1cdd
430620c
adddd50
1173ffd
6a09b3a
c799787
d1e445c
3f2df44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -753,6 +753,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-])" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an address uses an ASCII Punycode TLD, such as Useful? React with 👍 / 👎. |
||
| ) | ||
| _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]: | ||
| return {"uuid": str(uuid.uuid4())} | ||
|
|
||
|
|
@@ -769,7 +831,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]: | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| """Regression contract for bounded Korean and North American 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 | ||
|
|
||
|
|
||
| _SUPPORTED_PHONE_CASES = ( | ||
| ( | ||
| "국내 연락처는 010 1234 5678입니다.", | ||
| "국내 연락처는 [PHONE]입니다.", | ||
| ), | ||
| ( | ||
| "해외 표기는 +82 10 1234 5678입니다.", | ||
| "해외 표기는 [PHONE]입니다.", | ||
| ), | ||
| ( | ||
| "기존 표기는 010-1234-5678입니다.", | ||
| "기존 표기는 [PHONE]입니다.", | ||
| ), | ||
| ( | ||
| "북미 연락처는 +1 (123) 456-7890입니다.", | ||
| "북미 연락처는 [PHONE]입니다.", | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| @pytest.mark.parametrize(("source_text", "expected_text"), _SUPPORTED_PHONE_CASES) | ||
| async def test_email_phone_masker_masks_supported_phone_formats( | ||
| source_text: str, | ||
| expected_text: str, | ||
| ) -> None: | ||
| """Mask selected Korean and North American phone representations.""" | ||
| result = await email_phone_masker_handler({"text": source_text}) | ||
|
|
||
| assert result["masked_text"] == expected_text | ||
|
|
||
|
|
||
| @pytest.mark.parametrize(("source_text", "expected_text"), _SUPPORTED_PHONE_CASES) | ||
| def test_execute_email_phone_masker_masks_supported_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 |
Uh oh!
There was an error while loading. Please reload this page.