From 714616f12aa0133c70b90bc32d9a33f804cd01b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 01:12:23 +0900 Subject: [PATCH] fix(tools): remove unsafe phishing detector --- AGENTS.md | 6 +++ CHANGELOG.md | 9 ++++ backend/api/tools.py | 48 --------------------- backend/tests/test_tools_api.py | 76 ++++++++++++++++++--------------- 4 files changed, 56 insertions(+), 83 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 34ae68b8c..236b91f31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -506,6 +506,12 @@ in this repo. and workspace, restricted to an administrative role, and backed by an actual webhook or provider execution target. Never attach a mock handler or report successful execution when no external or local tool work occurred. +- Spam/phishing verdicts must be grounded in source-bound evidence and carry the + provider, evidence, and versioned provenance needed to audit the decision. Do + not advertise keyword matching or sender-domain suffix heuristics as a + phishing/spam detector. When the required provider or evidence is unavailable, + fail closed with an explicit typed `unknown`/unavailable result; never return a + benign boolean, risk score, or heuristic fallback. - Calendar UI actions must request `/api/calendar/writeback-intent` with server-authoritative source selection and provenance. Do not wire browser actions back to legacy `/api/calendar/sync` unless a trusted backend credential diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e609fb4..e48c88a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ ## [Unreleased] +### 이메일 보안 판정 경계 (Email Security Verdict Boundary) + +- 고정 키워드와 발신자 도메인 suffix만으로 확정적 spam/phishing boolean과 + risk score를 반환하던 `spam_phishing_detector`를 내장 도구 레지스트리에서 + 제거했습니다. 이를 대신하는 keyword·suffix fallback은 없으며, source-bound + evidence, provider verdict, provenance가 없는 경우에는 보안 판정을 생성하지 않고 + fail closed/unknown으로 처리해야 합니다. 아래의 과거 기능 추가 기록은 당시 변경 + 이력으로 보존하며 현재 지원 계약을 뜻하지 않습니다. + ### 도구 변경 경계 (Tool Mutation Boundary) - 프로세스 전역·비영속 레지스트리를 모든 인증 사용자가 변경할 수 있었던 diff --git a/backend/api/tools.py b/backend/api/tools.py index 032ec71c1..bcded79ef 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -256,43 +256,6 @@ async def email_translator_handler(params: Dict[str, Any]) -> Any: } -async def spam_phishing_detector_handler(params: Dict[str, Any]) -> Any: - """Score an email body for simple spam and phishing risk indicators.""" - email_content = params.get("email_content", "") - sender_domain = params.get("sender_domain", "") - normalized_content = email_content.lower() - normalized_domain = sender_domain.lower() - phishing_terms = {"password", "bank", "login", "verify", "account", "credential"} - spam_terms = {"urgent", "now", "free", "winner", "click", "limited"} - phishing_hits = sorted(term for term in phishing_terms if term in normalized_content) - spam_hits = sorted(term for term in spam_terms if term in normalized_content) - suspicious_domain = ( - normalized_domain.endswith((".ru", ".zip", ".tk")) - or "login" in normalized_domain - or "secure-" in normalized_domain - ) - risk_score = min( - 100, - 10 - + (20 * len(phishing_hits)) - + (15 * len(spam_hits)) - + (35 if suspicious_domain else 0), - ) - warnings: list[str] = [] - if phishing_hits: - warnings.append(f"phishing keywords detected: {', '.join(phishing_hits)}") - if spam_hits: - warnings.append(f"spam urgency keywords detected: {', '.join(spam_hits)}") - if suspicious_domain: - warnings.append(f"sender domain looks suspicious: {sender_domain}") - return { - "is_spam": bool(spam_hits or suspicious_domain), - "is_phishing": bool(len(phishing_hits) >= 2 or (phishing_hits and suspicious_domain)), - "risk_score": risk_score, - "warnings": warnings, - } - - async def reply_drafter_handler(params: Dict[str, Any]) -> Any: """Draft a formal reply using the operator's requested intent.""" original_email = params.get("original_email", "").strip() @@ -582,17 +545,6 @@ async def base64_decoder_handler(params: Dict[str, Any]) -> Dict[str, str]: email_translator_handler, ) -registry.register( - ToolInfo( - code="spam_phishing_detector", - name="스팸 및 피싱 탐지기 (Spam & Phishing Detector)", - description="이메일 본문과 발신자 도메인을 분석하여 스팸 및 피싱 위험도를 평가합니다.", - category="보안", - parameters={"email_content": "string", "sender_domain": "string"}, - ), - spam_phishing_detector_handler, -) - registry.register( ToolInfo( code="reply_drafter", diff --git a/backend/tests/test_tools_api.py b/backend/tests/test_tools_api.py index a4308d240..42d2b2e74 100644 --- a/backend/tests/test_tools_api.py +++ b/backend/tests/test_tools_api.py @@ -127,6 +127,47 @@ def test_get_tool_not_found(): assert response.json() == {"detail": "Tool not found"} +def test_startup_catalog_omits_unsupported_spam_phishing_detector(): + with TestClient(app) as client: + response = client.get( + "/api/tools", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + ) + + assert response.status_code == 200 + assert "spam_phishing_detector" not in { + tool["code"] for tool in response.json() + } + + +def test_removed_spam_phishing_detector_detail_returns_not_found(): + with TestClient(app) as client: + response = client.get( + "/api/tools/spam_phishing_detector", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + ) + + assert response.status_code == 404 + assert response.json() == {"detail": "Tool not found"} + + +def test_removed_spam_phishing_detector_execute_returns_not_found(): + with TestClient(app) as client: + response = client.post( + "/api/tools/spam_phishing_detector/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={ + "parameters": { + "email_content": "Urgent: update your bank password now", + "sender_domain": "secure-bank-login.ru", + } + }, + ) + + assert response.status_code == 404 + assert response.json() == {"detail": "Tool not found"} + + @pytest.mark.parametrize( "tool_code", ["email_categorizer", "meeting_agenda_generator"] ) @@ -894,27 +935,6 @@ def test_execute_email_translator(): assert data["result"]["source_language_detected"] == "en" -def test_execute_spam_phishing_detector(): - with TestClient(app) as client: - response = client.post( - "/api/tools/spam_phishing_detector/execute", - headers={"Authorization": f"Bearer {_signed_session_token()}"}, - json={ - "parameters": { - "email_content": "Urgent: update your bank password now", - "sender_domain": "secure-bank-login.ru", - } - }, - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == "success" - assert data["result"]["is_phishing"] is True - assert data["result"]["is_spam"] is True - assert data["result"]["risk_score"] >= 90 - assert any("sender domain" in warning for warning in data["result"]["warnings"]) - - def test_execute_reply_drafter(): with TestClient(app) as client: response = client.post( @@ -1012,7 +1032,6 @@ async def test_analysis_handlers_safe_and_fallthrough_paths(): email_translator_handler, grammar_checker_handler, sentiment_analyzer_handler, - spam_phishing_detector_handler, ) untranslated = await email_translator_handler( @@ -1021,19 +1040,6 @@ async def test_analysis_handlers_safe_and_fallthrough_paths(): assert untranslated["translated_text"] == "Hello, thank you for the meeting." assert untranslated["source_language_detected"] == "en" - safe_email = await spam_phishing_detector_handler( - { - "email_content": "Here are the approved meeting notes.", - "sender_domain": "example.com", - } - ) - assert safe_email == { - "is_spam": False, - "is_phishing": False, - "risk_score": 10, - "warnings": [], - } - nonurgent_negative = await sentiment_analyzer_handler( {"text": "I am disappointed."} )