From 7b152a85fc8ba7f4ec4848d795199e3b14e00870 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:13:03 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=8E=B8=EC=9D=98=EB=A5=BC=20=EC=9C=84=ED=95=9C=203=EA=B0=80?= =?UTF-8?q?=EC=A7=80=20=EC=8B=A0=EA=B7=9C=20=EB=8F=84=EA=B5=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 텍스트에서 URL을 추출하는 `url_extractor` 도구 추가 - 텍스트 내 이메일, 전화번호 등을 마스킹 처리하는 `pii_redactor` 추가 (한국어 호환) - 텍스트의 해시값(SHA256, MD5 등)을 생성하는 `hash_generator` 추가 - 위 세 가지 도구에 대한 유닛 테스트(100% 커버리지) 작성 - CHANGELOG.md에 신규 도구 관련 기능 추가 사항 업데이트 --- CHANGELOG.md | 4 ++ backend/api/tools.py | 62 ++++++++++++++++++++++ backend/tests/test_tools_api.py | 91 +++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+) 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..edad1074e 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -642,6 +642,68 @@ 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+", 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 + else: + h = hashlib.sha256() + + 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..d7f84ef4d 100644 --- a/backend/tests/test_tools_api.py +++ b/backend/tests/test_tools_api.py @@ -1252,3 +1252,94 @@ 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 "https://example.com" in data["result"]["urls"] + assert "http://test.org" in data["result"]["urls"] + assert data["result"]["url_count"] == 2 + +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" From 61495464ec38b3ecfbeddc17f0afc83d920384e7 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:33:09 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20CodeQL=20=EB=B3=B4=EC=95=88=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=A0=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20=EB=8F=84?= =?UTF-8?q?=EA=B5=AC=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CodeQL "arbitrary position in the sanitized URL" 경고 해결을 위해 테스트 코드 내 `in` 연산자를 배열 인덱싱 비교로 수정 - URL 추출 도구(`url_extractor_handler`)의 정규식을 개선하여 URL의 후행 구두점이 제외되도록 수정 - 해시 생성 도구(`hash_generator_handler`)에서 지원하지 않는 알고리즘 요청 시 `ValueError`가 발생하도록 방어 로직 추가 - 변경된 로직을 검증하는 테스트 케이스를 보강 및 100% 테스트 커버리지 유지 --- backend/api/tools.py | 6 ++++-- backend/tests/test_tools_api.py | 23 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/backend/api/tools.py b/backend/api/tools.py index edad1074e..82a5b8996 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -645,7 +645,7 @@ async def base64_decoder_handler(params: Dict[str, Any]) -> Dict[str, str]: async def url_extractor_handler(params: Dict[str, Any]) -> Any: text = params.get("text", "") - urls = re.findall(r"https?://\S+", text) + urls = re.findall(r"https?://[^\s,.]+(?:[.,][^\s,.]+)*", text) return {"urls": urls, "url_count": len(urls)} registry.register( @@ -687,8 +687,10 @@ async def hash_generator_handler(params: Dict[str, Any]) -> Any: h = hashlib.md5(usedforsecurity=False) # nosemgrep elif algorithm == "sha1": h = hashlib.sha1(usedforsecurity=False) # nosemgrep - else: + elif algorithm == "sha256": h = hashlib.sha256() + else: + raise ValueError("Unsupported hash algorithm") h.update(text.encode("utf-8")) return {"hash": h.hexdigest(), "algorithm": algorithm} diff --git a/backend/tests/test_tools_api.py b/backend/tests/test_tools_api.py index d7f84ef4d..9eb88b240 100644 --- a/backend/tests/test_tools_api.py +++ b/backend/tests/test_tools_api.py @@ -1261,16 +1261,16 @@ def test_execute_url_extractor(): headers={"Authorization": f"Bearer {_signed_session_token()}"}, json={ "parameters": { - "text": "Checkout my new website: https://example.com and http://test.org for more info." + "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 "https://example.com" in data["result"]["urls"] - assert "http://test.org" in data["result"]["urls"] 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: @@ -1343,3 +1343,20 @@ def test_execute_hash_generator(): 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