-
Notifications
You must be signed in to change notification settings - Fork 1
feat(tools): URL 추출기 및 개인정보 마스킹 도구 추가 #1487
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
Changes from all commits
9387554
7c1e76b
144fd0b
3bb9350
e011af4
4c77915
95bce03
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 |
|---|---|---|
|
|
@@ -403,6 +403,47 @@ async def grammar_checker_handler(params: Dict[str, Any]) -> Any: | |
| "suggestions": suggestions, | ||
| } | ||
|
|
||
| async def url_extractor_handler(params: Dict[str, Any]) -> Any: | ||
| text = params["text"] | ||
| urls = re.findall(r'https?://[^\s<>"]+|www\.[^\s<>"]+', text) | ||
|
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.
Comment on lines
+407
to
+408
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.
Contributor
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Strip trailing URL delimiters. The pattern accepts all non-space characters except 🤖 Prompt for AI Agents |
||
| return {"urls": 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["text"] | ||
|
|
||
| # Mask emails | ||
| redacted_text = re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '[EMAIL]', text) | ||
|
|
||
| # Mask phone numbers (simple pattern for various formats like 010-1234-5678, +82 10 1234 5678, etc) | ||
| redacted_text = re.sub(r'(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,3}\)?[-.\s]?\d{3,4}[-.\s]?\d{4}', '[PHONE]', redacted_text) | ||
|
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.
Contributor
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. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: printf '%s\n' '--- applicable repository knowledge files ---'
find /tmp/coderabbit-repo-knowledge/contextualwisdomlab-naruon-4f5ba5de \
-maxdepth 2 -type f \( -path '*/contextualwisdomlab-naruon-4f5ba5de/*/*.md' -o -path '*/contextualwisdomlab-naruon-4f5ba5de/*' \) 2>/dev/null \
| sort | head -80
printf '%s\n' '--- tools.py outline ---'
ast-grep outline backend/api/tools.py 2>/dev/null | head -120
printf '%s\n' '--- target context ---'
sed -n '380,465p' backend/api/tools.py
printf '%s\n' '--- relevant references ---'
rg -n -C 3 'redacted_text|010-1234-5678|PHONE|redact' backend tests 2>/dev/null | head -240Repository: ContextualWisdomLab/naruon Length of output: 22718 🏁 Script executed: printf '%s\n' '--- backend conventions ---'
cat /tmp/coderabbit-repo-knowledge/contextualwisdomlab-naruon-4f5ba5de/conventions/backend.md
printf '%s\n' '--- repo-wide conventions ---'
cat /tmp/coderabbit-repo-knowledge/contextualwisdomlab-naruon-4f5ba5de/conventions/repo-wide.md
printf '%s\n' '--- focused tool tests ---'
git ls-files backend/tests | rg '(^|/)(test_.*tool|.*tool.*test|test_api).*\.py$' || true
rg -n -C 5 'execute_tool|pii_redactor|010-1234-5678' backend/api/tools.py backend/tests --glob '*.py' | head -260
printf '%s\n' '--- execute route context ---'
sed -n '895,930p' backend/api/tools.pyRepository: ContextualWisdomLab/naruon Length of output: 17360 🏁 Script executed: python3 - <<'PY'
import re
pattern = r'(?:\+?\d{1,3}[-.\s]?)?\(?\d{2,3}\)?[-.\s]?\d{3,4}[-.\s]?\d{4}'
for text in ("01 42 68 53 00", "010-1234-5678", "+82 10 1234 5678"):
print(f"{text!r} -> {re.sub(pattern, '[PHONE]', text)!r}")
PYRepository: ContextualWisdomLab/naruon Length of output: 262 Sensitive Data Exposure (CWE-359) Reachability: External Prevent phone-number under-redaction. Add support for required locale-specific formats, including 🤖 Prompt for AI Agents |
||
|
|
||
| return {"redacted_text": redacted_text} | ||
|
|
||
|
|
||
| registry.register( | ||
| ToolInfo( | ||
| code="pii_redactor", | ||
| name="개인정보 마스킹 (PII Redactor)", | ||
| description="텍스트 본문에서 이메일 주소와 전화번호 등 개인정보를 마스킹 처리합니다.", | ||
| category="보안", | ||
| parameters={"text": "string"}, | ||
| ), | ||
| pii_redactor_handler, | ||
| ) | ||
|
|
||
|
|
||
| def is_safe_webhook_url(url: str) -> bool: | ||
| try: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,6 @@ | |
| from dataclasses import dataclass | ||
| from pathlib import Path | ||
| from typing import Any | ||
| from urllib.parse import unquote | ||
|
|
||
| from .text_safety import strip_html_markup | ||
|
|
||
|
|
@@ -17,7 +16,6 @@ | |
| } | ||
| MAX_ATTACHMENT_PARSE_SOURCE_CHARS = 1_000_000 | ||
| MAX_ATTACHMENT_PARSE_SOURCE_BYTES = 20 * 1024 * 1024 | ||
| MAX_ATTACHMENT_FILENAME_DECODE_ROUNDS = 3 | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
|
|
@@ -268,19 +266,8 @@ def _parser_key_for(parse_content_type: str, parse_status: str) -> str: | |
|
|
||
| def _safe_filename(filename: str | None) -> str: | ||
| """Return a basename-only attachment display filename.""" | ||
| display_filename = filename or "attachment" | ||
| for _ in range(MAX_ATTACHMENT_FILENAME_DECODE_ROUNDS): | ||
| decoded_filename = unquote(display_filename) | ||
| if decoded_filename == display_filename: | ||
| break | ||
| display_filename = decoded_filename | ||
| # Entity-encoded percent escapes (for example ``%2e``) only become | ||
| # literal ``%`` sequences during markup decoding, so the residual-encoding | ||
| # guard must run after ``strip_html_markup`` to stay fail-closed. | ||
| display_filename = strip_html_markup(_sanitize_nul(display_filename)) | ||
| if unquote(display_filename) != display_filename: | ||
| return "attachment" | ||
| display_filename = Path(display_filename.replace("\\", "/")).name.strip() | ||
| display_filename = strip_html_markup(_sanitize_nul(filename or "attachment")) | ||
| display_filename = Path(display_filename).name.strip() | ||
|
Comment on lines
+269
to
+270
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. |
||
| if display_filename in {"", ".", ".."}: | ||
| return "attachment" | ||
| return display_filename | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,6 @@ | |
| import pytest | ||
|
|
||
| from services.attachment_parser import ( | ||
| _safe_filename, | ||
| MAX_ATTACHMENT_PARSE_SOURCE_BYTES, | ||
| MAX_ATTACHMENT_PARSE_SOURCE_CHARS, | ||
| decode_deferred_attachment_payload, | ||
|
|
@@ -256,28 +255,3 @@ def test_deferred_pdf_decoder_rejects_non_pdf_and_oversized_payloads(monkeypatch | |
| oversized = base64.b64encode(b"%PDF-1.7").decode("ascii") | ||
|
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. |
||
| with pytest.raises(ValueError, match="size limit"): | ||
| decode_deferred_attachment_payload(oversized) | ||
|
|
||
|
|
||
| def test_safe_filename_handles_windows_path_traversal(): | ||
| assert _safe_filename("..\\..\\upload.txt") == "upload.txt" | ||
| assert _safe_filename("C:\\mail\\report.pdf") == "report.pdf" | ||
| assert _safe_filename("%5c%2e%2e%5csecret.txt") == "secret.txt" | ||
| assert _safe_filename("%252e%252e%252fsecret.txt") == "secret.txt" | ||
| assert _safe_filename("%252525252e%252525252e%252525252fsecret.txt") == "attachment" | ||
|
|
||
|
|
||
| def test_safe_filename_fails_closed_after_entity_decoding(): | ||
| """Entity-encoded percent escapes must trip the residual guard post-decode.""" | ||
| assert _safe_filename("%2e%2e%2fsecret.txt") == "attachment" | ||
|
|
||
|
|
||
| def test_safe_filename_plain_percent_encoded_traversal_still_decodes_to_basename(): | ||
| """Single percent-encoded traversal still decodes in-round to its basename.""" | ||
| assert _safe_filename("%2e%2e%2fsecret.txt") == "secret.txt" | ||
|
|
||
|
|
||
| def test_safe_filename_benign_name_survives_unchanged(): | ||
| assert _safe_filename("annual-report-2026.pdf") == "annual-report-2026.pdf" | ||
| assert _safe_filename("quarterly report & notes.pdf") == ( | ||
| "quarterly report & notes.pdf" | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1078,6 +1078,42 @@ def test_execute_grammar_checker(): | |
| assert data["result"]["errors_found"] == 3 | ||
|
|
||
|
|
||
| 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": "Check out https://example.com and www.google.com for more info." | ||
| } | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert data["status"] == "success" | ||
| urls = data["result"]["urls"] | ||
| assert len(urls) == 2 | ||
| assert set(urls) == {"https://example.com", "www.google.com"} | ||
|
|
||
|
|
||
| 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": "Contact me at test@example.com or 010-1234-5678." | ||
| } | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| data = response.json() | ||
| assert data["status"] == "success" | ||
| assert data["result"]["redacted_text"] == "Contact me at [EMAIL] or [PHONE]." | ||
|
Comment on lines
+1081
to
+1114
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. |
||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_mock_handler(): | ||
| from api.tools import mock_handler | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Feature lacks required research grounding
The repository requires substantive features to include relevant research citations and permissible PDFs. This extraction and redaction feature adds none.
Was this helpful? React with 👍 or 👎 to provide feedback.