Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 등의 실용적인 유틸리티 도구들을 추가했습니다.

Expand Down
64 changes: 64 additions & 0 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add complete docstrings and concrete return types for all three public handlers, then add input-type and bounded-size validation. params.get(..., "") currently accepts non-string values that can raise implementation-specific exceptions, and these new public functions violate the repository's full-docstring contract.

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"(?<![a-zA-Z0-9._%+-])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?![a-zA-Z0-9._%+-])")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool is advertised as a PII redactor but only recognizes one ASCII email pattern and one hyphenated Korean phone format. That creates a dangerous false assurance for spaces, parentheses, country codes, internationalized email domains, resident-registration-like identifiers, and malformed-but-sensitive values. Either narrow the product name/description to contact-data redaction or implement and document a versioned detector scope, return redaction counts/types, enforce input limits, and add realistic false-positive/false-negative regression cases.

phone_pattern = re.compile(r"(?<!\d)\d{2,3}-\d{3,4}-\d{4}(?!\d)")

redacted = email_pattern.sub("[REDACTED EMAIL]", text)
redacted = phone_pattern.sub("[REDACTED PHONE]", redacted)
return {"redacted_text": redacted}

registry.register(
ToolInfo(
code="pii_redactor",
name="개인정보 마스킹 (PII Redactor)",
description="텍스트에서 이메일 및 전화번호와 같은 개인정보를 식별하고 비식별화 처리합니다.",
category="보안",
parameters={"text": "string"},
),
pii_redactor_handler,
)

async def hash_generator_handler(params: Dict[str, Any]) -> Any:
text = params.get("text", "")
algorithm = params.get("algorithm", "sha256").lower()

if algorithm == "md5":

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not present MD5/SHA-1 under the 보안 category without an explicit non-security contract. Move this to a checksum/utility category, return a machine-readable security_use_allowed: false flag for legacy algorithms (or restrict the API to SHA-256+), and add tests for Unicode normalization policy, empty input, mixed-case algorithm names, invalid parameter types, and size limits.

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",
Expand Down
108 changes: 108 additions & 0 deletions backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading