diff --git a/CHANGELOG.md b/CHANGELOG.md index a06003d8f..e2badfe22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ - `reply_drafter`: 이전 맥락을 기반으로 답장 초안 자동 생성 - `sentiment_analyzer`: 이메일의 전반적인 감정(긍정/부정) 분석 - `grammar_checker`: 작성된 이메일 초안의 문법과 철자 교정 +- **추가 도구 구현 완료**: 다음과 같은 3가지 유용한 도구를 추가로 개발하여 등록했습니다. + - `url_extractor`: 사용자 텍스트 본문 내에서 URL(https/http)을 정규식으로 추출하고 집계합니다. + - `pii_redactor`: 이메일과 전화번호 같은 개인정보(PII)를 자동으로 식별하고 비식별화 처리(Masking)합니다. 한국어 텍스트와 호환되도록 명시적 경계(Lookaround)를 사용했습니다. + - `hash_generator`: 입력 텍스트의 해시값(SHA256, SHA1, MD5 등)을 생성하며, Bandit 검사 보안 규칙(`usedforsecurity=False`)을 준수합니다. - 각 신규 도구 핸들러에 대해 100% 테스트 커버리지를 보장하는 개별 테스트를 `backend/tests/test_tools_api.py`에 추가했습니다. - `text_analyzer`, `base64_encoder`, `base64_decoder` 등의 실용적인 유틸리티 도구들을 추가했습니다. diff --git a/backend/api/tools.py b/backend/api/tools.py index eafbaaf76..82a5b8996 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -642,6 +642,70 @@ async def base64_decoder_handler(params: Dict[str, Any]) -> Dict[str, str]: sentiment_analyzer_handler, ) + +async def url_extractor_handler(params: Dict[str, Any]) -> Any: + text = params.get("text", "") + urls = re.findall(r"https?://[^\s,.]+(?:[.,][^\s,.]+)*", text) + return {"urls": urls, "url_count": len(urls)} + +registry.register( + ToolInfo( + code="url_extractor", + name="URL 추출기 (URL Extractor)", + description="텍스트 본문에서 모든 URL을 추출합니다.", + category="텍스트 분석", + parameters={"text": "string"}, + ), + url_extractor_handler, +) + +async def pii_redactor_handler(params: Dict[str, Any]) -> Any: + text = params.get("text", "") + email_pattern = re.compile(r"(? Any: + text = params.get("text", "") + algorithm = params.get("algorithm", "sha256").lower() + + if algorithm == "md5": + h = hashlib.md5(usedforsecurity=False) # nosemgrep + elif algorithm == "sha1": + h = hashlib.sha1(usedforsecurity=False) # nosemgrep + elif algorithm == "sha256": + h = hashlib.sha256() + else: + raise ValueError("Unsupported hash algorithm") + + h.update(text.encode("utf-8")) + return {"hash": h.hexdigest(), "algorithm": algorithm} + +registry.register( + ToolInfo( + code="hash_generator", + name="해시 생성기 (Hash Generator)", + description="텍스트의 해시값(SHA256, MD5 등)을 생성합니다.", + category="보안", + parameters={"text": "string", "algorithm": "string"}, + ), + hash_generator_handler, +) + registry.register( ToolInfo( code="grammar_checker", diff --git a/backend/tests/test_tools_api.py b/backend/tests/test_tools_api.py index ae5c0a396..9eb88b240 100644 --- a/backend/tests/test_tools_api.py +++ b/backend/tests/test_tools_api.py @@ -1252,3 +1252,111 @@ def test_execute_analysis_tool_rejects_oversized_text(): f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters" ), } + + +def test_execute_url_extractor(): + with TestClient(app) as client: + response = client.post( + "/api/tools/url_extractor/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={ + "parameters": { + "text": "Checkout my new website: https://example.com. and http://test.org, for more info." + } + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["result"]["url_count"] == 2 + assert data["result"]["urls"][0] == "https://example.com" + assert data["result"]["urls"][1] == "http://test.org" + +def test_execute_pii_redactor(): + with TestClient(app) as client: + response = client.post( + "/api/tools/pii_redactor/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={ + "parameters": { + "text": "내 이메일은 test@example.com 이고, 전화번호는 010-1234-5678입니다." + } + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert "test@example.com" not in data["result"]["redacted_text"] + assert "010-1234-5678" not in data["result"]["redacted_text"] + assert "[REDACTED EMAIL]" in data["result"]["redacted_text"] + assert "[REDACTED PHONE]" in data["result"]["redacted_text"] + +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", + "algorithm": "sha256" + } + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + # echo -n "hello" | sha256sum -> 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 + assert data["result"]["hash"] == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + assert data["result"]["algorithm"] == "sha256" + + with TestClient(app) as client: + response2 = client.post( + "/api/tools/hash_generator/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={ + "parameters": { + "text": "hello", + "algorithm": "md5" + } + }, + ) + assert response2.status_code == 200 + data2 = response2.json() + assert data2["status"] == "success" + # echo -n "hello" | md5sum -> 5d41402abc4b2a76b9719d911017c592 + assert data2["result"]["hash"] == "5d41402abc4b2a76b9719d911017c592" + + with TestClient(app) as client: + response3 = client.post( + "/api/tools/hash_generator/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={ + "parameters": { + "text": "hello", + "algorithm": "sha1" + } + }, + ) + assert response3.status_code == 200 + data3 = response3.json() + assert data3["status"] == "success" + # echo -n "hello" | sha1sum -> aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d + assert data3["result"]["hash"] == "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d" + + with TestClient(app) as client: + response4 = client.post( + "/api/tools/hash_generator/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={ + "parameters": { + "text": "hello", + "algorithm": "sha512" + } + }, + ) + assert response4.status_code == 200 + data4 = response4.json() + assert data4["status"] == "failed" + assert "Unsupported hash algorithm" in data4["message"] + assert data4.get("result") is None