-
Notifications
You must be signed in to change notification settings - Fork 0
Feat(#36): 형태소·문자 기반 피싱 분류 모델 추가 및 성능 비교 (1/2) #38
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
base: develop
Are you sure you want to change the base?
Changes from all commits
d31b813
cd0d95b
36a247a
13ac486
9626528
82dce94
2207354
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,149 +1,167 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 사전 학습된 Naive Bayes 모델을 이용한 SMS 피싱 분석기 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from scipy.sparse import csr_matrix, hstack | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.analysis.risk_policy import determine_text_risk_grade | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.analysis.text.preprocessing import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extract_struct_features, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalize_text, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.core.config import settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MODEL_PATH = settings.NAIVE_BAYES_MODEL_PATH | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VECTORIZER_PATH = settings.NAIVE_BAYES_VECTORIZER_PATH | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- 전처리 정규식 : data_science/SMSModel/train_sms.py의 정규화/피처 추출 로직과 반드시 동일하게 유지 --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # (학습 시 벡터라이저가 본 입력 분포와 서빙 시 입력 분포가 어긋나면 모델이 무의미해짐) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_URL = re.compile(r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_RRN = re.compile(r"(?<!\d)\d{6}[- ]\d{7}(?!\d)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_CARD = re.compile(r"(?<!\d)(?:\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}|\d{4}[- ]?\d{6}[- ]?\d{5})(?!\d)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_PHONE = re.compile(r"(?<!\d)(?:0\d{1,2}[- ]?\d{3,4}[- ]?\d{4}|0\d{9,10})(?!\d)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_ACCOUNT = re.compile(r"(?<!\d)\d{2,6}-\d{2,6}-\d{2,6}(?:-\d{1,6})?(?!\d)|(?<!\d)\d{10,14}(?!\d)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_EMAIL = re.compile(r"(?i)[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_AMOUNT = re.compile(r"\d+[,\d]*원") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_FORMAT_ARTIFACT = re.compile(r"={2,}|■|□|▪|▫|●|○|\s-\s|\s:\s") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_SHORT_URL = re.compile(r"bit\.ly|goo\.gl|tinyurl|gourl|ow\.ly|n\.bnuee|han\.gl|cutt\.ly") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _RE_WEB_TAG = re.compile(r"\[Web발신\]|\[국외발신\]|\[국제발신\]") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_ANALYSIS_RESULT = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "grade": "UNKNOWN", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "risk_score": 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "is_suspected_phishing": False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_message": "나이브 베이즈 모델을 로드하지 못해 위험도를 판정할 수 없습니다." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_message": ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "나이브 베이즈 모델을 로드하지 못해 " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "위험도를 판정할 수 없습니다." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _model = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _vectorizer = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _threshold = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _classes = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_error: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_attempted = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_error: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _normalize_text(text: str) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_end = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for m in _RE_URL.finditer(text): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append(_mask_pii(text[last_end:m.start()])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append("[URL]") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_end = m.end() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append(_mask_pii(text[last_end:])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = "".join(parts) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_AMOUNT.sub("[AMOUNT]", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_FORMAT_ARTIFACT.sub(" ", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return re.sub(r"\s+", " ", text).strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _mask_pii(text: str) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_RRN.sub("[RRN]", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_CARD.sub("[CARD]", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_PHONE.sub("[PHONE]", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_ACCOUNT.sub("[ACCOUNT]", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text = _RE_EMAIL.sub("[EMAIL]", text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return text | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _extract_struct_features(text: str) -> list: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(bool(_RE_URL.search(text))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(bool(_RE_SHORT_URL.search(text))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(bool(_RE_PHONE.search(text) or "[PHONE]" in text)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(bool(_RE_AMOUNT.search(text))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(bool(_RE_WEB_TAG.search(text))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| int(len(text) > 100), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_attempted = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _load_artifacts() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """FastAPI 프로세스 당 1회만 시도. 실패 시 재시도하지 않고 fail-safe 응답으로 대체.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _model, _vectorizer, _threshold, _classes, _load_error, _load_attempted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 모델과 벡터라이저를 프로세스당 한 번만 로드 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 로드에 실패해도 API 서버 전체를 중단시키지 않고, 이후 분석 요청에서 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UNKNOWN 결과를 반환할 수 있도록 오류 상태만 저장 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _vectorizer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _threshold | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _classes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _load_error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global _load_attempted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _load_attempted: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_attempted = True | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
58
to
61
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. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Concurrent first requests can receive a spurious unavailable result.
Guard the load with a lock and re-check the flag inside it. 🔒 Proposed fix import logging
+import threading
import numpy as np+_load_lock = threading.Lock()
_load_attempted = False- if _load_attempted:
- return
-
- _load_attempted = True
-
- try:
- import joblib
-
- artifact = joblib.load(MODEL_PATH)
- vectorizer = joblib.load(VECTORIZER_PATH)
-
- # 모든 값이 정상적으로 읽힌 이후 전역 상태를 갱신
- _model = artifact["model"]
- _threshold = artifact["threshold"]
- _classes = artifact["classes"]
- _vectorizer = vectorizer
- _load_error = None
-
- logger.info(
- "[NaiveBayes] 모델 로드 완료 (threshold=%s)",
- _threshold,
- )
- except Exception as exception:
- _load_error = type(exception).__name__
-
- logger.error(
- "[NaiveBayes] 모델 로드 실패. error_type=%s",
- _load_error,
- )
+ if _load_attempted:
+ return
+
+ with _load_lock:
+ if _load_attempted:
+ return
+
+ try:
+ import joblib
+
+ artifact = joblib.load(MODEL_PATH)
+ vectorizer = joblib.load(VECTORIZER_PATH)
+
+ # 모든 값이 정상적으로 읽힌 이후 전역 상태를 갱신
+ _model = artifact["model"]
+ _threshold = artifact["threshold"]
+ _classes = artifact["classes"]
+ _vectorizer = vectorizer
+ _load_error = None
+
+ logger.info(
+ "[NaiveBayes] 모델 로드 완료 (threshold=%s)",
+ _threshold,
+ )
+ except Exception as exception:
+ _load_error = type(exception).__name__
+
+ logger.error(
+ "[NaiveBayes] 모델 로드 실패. error_type=%s",
+ _load_error,
+ )
+ finally:
+ _load_attempted = True📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import joblib | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| artifact = joblib.load(MODEL_PATH) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vectorizer = joblib.load(VECTORIZER_PATH) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 모든 값이 정상적으로 읽힌 이후 전역 상태를 갱신 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _model = artifact["model"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _threshold = artifact["threshold"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _classes = artifact["classes"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _vectorizer = joblib.load(VECTORIZER_PATH) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"[NaiveBayes] 모델 로드 완료 (threshold={_threshold})") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _vectorizer = vectorizer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_error = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "[NaiveBayes] 모델 로드 완료 (threshold=%s)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _threshold, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_error = type(exception).__name__ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "[NaiveBayes] 모델 로드 실패. error_type=%s", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def is_model_loaded() -> bool: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Naive Bayes 모델을 사용할 수 있는지 반환""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_artifacts() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return _model is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 사전 학습된 나이브 베이즈(ComplementNB + isotonic 보정) 모델로 문자 메시지의 1차 위험도를 산출 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def analyze_text_with_naive_bayes(text: str) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """사전 학습된 Naive Bayes 모델로 SMS의 피싱 위험도를 분석""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_artifacts() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _model is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "engine": "naive_bayes", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "is_available": False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "result": dict(DEFAULT_ANALYSIS_RESULT, error_message=_load_error or DEFAULT_ANALYSIS_RESULT["error_message"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "result": dict( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_ANALYSIS_RESULT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error_message=( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _load_error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| or DEFAULT_ANALYSIS_RESULT["error_message"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+103
to
110
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 | ⚡ Quick win Do not return the exception class name as the user-facing error message.
🛡️ Proposed fix "result": dict(
DEFAULT_ANALYSIS_RESULT,
- error_message=(
- _load_error
- or DEFAULT_ANALYSIS_RESULT["error_message"]
- ),
+ error_message=DEFAULT_ANALYSIS_RESULT["error_message"],
),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from scipy.sparse import csr_matrix, hstack | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 학습과 동일한 공통 전처리를 적용 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalized_text = normalize_text(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_norm = _normalize_text(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct = np.array([_extract_struct_features(text)]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct_features = np.asarray( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [extract_struct_features(text)], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dtype=np.int8, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+116
to
+119
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 | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Compare the dataset has_url column against the shared URL regex.
fd -t f 'phishing_total_dataset_2705.csv' | head -n 1 | while IFS= read -r csv; do
python - "$csv" <<'PY'
import sys, pandas as pd
sys.path.insert(0, ".")
from app.analysis.text.preprocessing import URL_PATTERN
df = pd.read_csv(sys.argv[1])
detected = df["text"].astype(str).str.contains(URL_PATTERN)
declared = df["has_url"].astype(bool)
print("rows:", len(df), "mismatches:", int((detected != declared).sum()))
print(df.loc[detected != declared, ["text", "has_url"]].head(10).to_string())
PY
doneRepository: SafeFam/SafeFam_AI Length of output: 275 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== files ==="
fd -t f 'naive_bayes_analyzer.py|train_sms.py|naive_bayes.py|preprocessing.py|phishing_total_dataset_2705.csv' .
echo
echo "=== relevant snippets ==="
for f in app/analysis/text/naive_bayes_analyzer.py data_science/SMSModel/train_sms.py data_science/SMSModel/modeling/naive_bayes.py app/analysis/text/preprocessing.py; do
if [ -f "$f" ]; then
echo "--- $f ---"
ast-grep outline "$f" --match 'extract_struct_features|extract_struct_feature_matrix|_build_feature_matrix|URL_PATTERN' --view expanded || true
rg -n -C 4 'def extract_struct_features|def extract_struct_feature_matrix|def _build_feature_matrix|has_url|URL_PATTERN' "$f"
fi
done
echo
echo "=== dataset has_url and text-based count (no pandas) ==="
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
if [ -n "${csv:-}" ]; then
python3 - "$csv" <<'PY'
import sys, re
csv=sys.argv[1]
with open(csv, encoding='utf-8') as f:
header=f.readline().strip().split(',')
has_text=header.index('text')
has_url=header.index('has_url')
rows=list(csv.strip().splitlines())
print("csv:", csv, "rows:", len(rows)-1)
# read exact columns without pandas
PY
url_pattern="$(python3 -c "import ast, pathlib, re; f=pathlib.Path('app/analysis/text/preprocessing.py'); tree=ast.parse(f.read_text()); constants={n.value.id:'SUFFIX_PATTERN' in n.value.value if isinstance(n.value, ast.BinOp) else n.value.value for n in ast.walk(tree) if isinstance(n, ast.Assign)}; print(repr(__import__('glob').glob('SUFFIX_PATTERN')[0] if False else ''))")"
python3 - "$csv" <<'PY'
import sys, re, ast, pathlib
csv=sys.argv[1]
mod=pathlib.Path('app/analysis/text/preprocessing.py')
source=mod.read_text()
tree=ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'URL_PATTERN':
print("URL_PATTERN=", ast.literal_eval(node.value.value))
PY
python3 - <<'PY' "$csv"
import sys, re, ast, pathlib
csv=sys.argv[1]
mod=pathlib.Path('app/analysis/text/preprocessing.py')
source=mod.read_text()
tree=ast.parse(source)
url_re=None
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'URL_PATTERN':
url_re=ast.literal_eval(node.value.value)
p=re.compile(url_re)
with open(csv, encoding='utf-8') as f:
header=f.readline().strip().split(',')
has_text=header.index('text')
has_url=header.index('has_url')
rows=list(csv.strip().splitlines())
print("rows:", len(rows)-1, "url_re:", url_re)
mism=0
examples=[]
for i,row in enumerate(rows, start=2):
fields=row.split(',', header.index('has_url')+1)
txt=fields[has_text]
declared=bool(int(fields[has_url])) if fields[has_url] else False
detected=bool(p.search(txt))
if declared != detected:
mism += 1
if len(examples) < 10:
examples.append((i, txt[:200], declared, detected))
print("mismatches:", mism)
for rec in examples:
print(rec)
PY
else
echo "no dataset found"
fiRepository: SafeFam/SafeFam_AI Length of output: 683 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== source snippets ==="
for f in app/analysis/text/naive_bayes_analyzer.py data_science/SMSModel/train_sms.py data_science/SMSModel/modeling/naive_bayes.py app/analysis/text/preprocessing.py; do
if [ -f "$f" ]; then
echo "--- $f ---"
wc -l "$f"
rg -n -C 4 'extract_struct_features|extract_struct_feature_matrix|build_feature_matrix|_build_feature_matrix|has_url|URL_PATTERN' "$f" || true
fi
done
echo
echo "=== URL_PATTERN ==="
cd app/analysis/text
python3 - <<'PY'
import ast
from pathlib import Path
source = Path('preprocessing.py').read_text()
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'URL_PATTERN':
print(ast.literal_eval(node.value.value))
PY
echo
echo "=== dataset count without pandas ==="
csv="$(fd -t f 'phishing_total_dataset_2705.csv' data_science/Data/SMSData | head -n 1 || true)"
if [ -n "${csv:-}" ]; then
python3 - "$csv" <<'PY'
import re, ast, sys, pathlib
csv = sys.argv[1]
source = pathlib.Path('app/analysis/text/preprocessing.py').read_text()
tree = ast.parse(source)
url_re = None
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == 'URL_PATTERN':
url_re = ast.literal_eval(node.value.value)
print("url_re:", url_re)
p = re.compile(url_re)
url_index = None
with open(csv, encoding='utf-8') as f:
header = f.readline().strip().split(',')
for i, h in enumerate(header):
if h.startswith('contains_url') or h == 'has_url' or h == 'has_url?' or h == 'has_url_':
url_index = i
break
if url_index is None:
print("no contains_url/has_url column")
sys.exit(0)
text_index = header.index('text') if 'text' in header else -1
rows = []
for line in f:
fields = line.strip().split(',', header.index('contains_url?') + 1)
text = fields[text_index]
rows.append((bool(p.search(text)), bool(int(fields[url_index]))))
same = sum(a == b for a, b in rows)
print("rows:", len(rows), "same:", same, "mismatches:", len(rows) - same)
mismatches = [(a,b) for a,b in rows if a != b]
if mismatches:
print("mismatch_fraction:", len(mismatches)/len(rows))
print("first_mismatches:", mismatches[:10])
PY
fiRepository: SafeFam/SafeFam_AI Length of output: 8073 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re, sys, ast, pathlib
csv = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
try:
idx = next(i for i, h in enumerate(open(csv, encoding='utf-8').readline().strip().split(',')) if h == 'has_url')
except StopIteration:
print("missing has_url")
sys.exit(0)
with open(csv, encoding='utf-8') as f:
header = f.readline().strip().split(',')
text_idx = header.index('text')
row_count = 0
same = 0
examples = []
for line in f:
row_count += 1
fields = line.rstrip('\n').split(',', idx + 1)
detected = bool(p.search(fields[text_idx]))
declared = bool(int(fields[idx])) if fields[idx] else False
if detected != declared:
if len(examples) < 10:
examples.append((row_count, decoded(fields[text_idx])[1][100:], declared, detected))
else:
same += 1
def decoded(s):
try: return bytes.fromhex(s).decode('utf-8', 'replace')
except Exception: return s
print("rows:", row_count, "same:", same, "mismatches:", row_count - same)
for e in examples:
print(e)
PY
data_science/Data/SMSData/phishing_total_dataset_2705.csvRepository: SafeFam/SafeFam_AI Length of output: 265 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
echo "=== dataset header ==="
if [ -n "${csv:-}" ]; then
python3 - "$csv" <<'PY'
import sys
path = sys.argv[1]
with open(path, encoding='utf-8') as f:
line = f.readline()
print(line.strip())
PY
echo "url-pattern match counts"
python3 - "$csv" <<'PY'
import re, sys
p = re.compile(r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+")
path = sys.argv[1]
with open(path, encoding='utf-8') as f:
header = f.readline().strip().split(',')
print("header_count=", len(header), "contains_url_position=", next((i,header[i]) for i,h in enumerate(header) if h=="contains_url?"), sep="#")
text_i=header.index('text')
contains_url_i=header.index('contains_url?')
total=0; ppos=0; npos=0; mism=0
for i,line in enumerate(f,2):
total += 1
fields=line.rstrip('\n').split(',')
text=fields[text_i]
declared=bool(int(fields[contains_url_i])) if fields[contains_url_i] else False
detected=bool(p.search(text))
if declared:
ppos +=1
else:
npos +=1
if declared != detected:
mism +=1
if mism <= 5:
print(f"row {i}: txt_len={len(text)} declared={declared} detected={detected} txt={text[:120]}")
print("total_rows", total, "pos_declared", ppos, "neg_declared", npos, "same", total-mism, "mismatches", mism)
PY
fiRepository: SafeFam/SafeFam_AI Length of output: 323 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
echo "=== dataset header =="
if [ -n "${csv:-}" ]; then
python3 - "$csv" <<'PY'
import sys
path = sys.argv[1]
with open(path, encoding='utf-8') as f:
line = f.readline()
print(line.strip())
print("has_url position:", next((i, h) for i, h in enumerate(line.strip().split(',')) if h == 'has_url'))
PY
echo "=== url-pattern vs has_url =="
python3 - "$csv" <<'PY'
import re, sys, ast, pathlib
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8') as f:
header = f.readline().strip().split(',')
text_i = header.index('text')
has_url_i = header.index('has_url')
total = same = mism = 0
examples = []
for i, line in enumerate(f, 2):
total += 1
fields = line.rstrip('\n').split(',')
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
if declared == detected:
same += 1
else:
mism += 1
if len(examples) < 10:
examples.append((i, len(txt), declared, detected, txt[:80]))
print("rows", total)
print("same", same)
print("mismatches", mism)
print("mismatches_fraction", mism / total)
for ex in examples:
print(ex)
PY
fiRepository: SafeFam/SafeFam_AI Length of output: 380 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
echo "=== column indices for URL_PATTERN regex used in preprocessing.py =="
python3 - "$csv" <<'PY'
import re, sys
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8') as f:
header = f.readline().strip().split(',')
print(header)
text_i = next(i for i, h in enumerate(header) if h == 'text')
has_url_i = next(i for i, h in enumerate(header) if h == 'has_url')
text_escaped_re = re.escape(header[text_i])
has_url_escaped_re = re.escape(header[has_url_i])
print(text_i)
print(has_url_i)
match = p.search(header[has_url_i])
print(match) if match else print("URL_PATTERN matches header's has_url column")
PY
echo "=== raw head =="
python3 - "$csv" <<'PY'
import sys
path = sys.argv[1]
with open(path, encoding='utf-8', newline='') as f:
for i in range(5):
print(f"line[{i}] repr={next(f).strip()!r}")
PY
echo "=== URL_FEATURE from contains_url-like fields if any =="
python3 - "$csv" <<'PY'
import re, sys
path = sys.argv[1]
with open(path, encoding='utf-8') as f:
header = f.readline().strip().split(',')
text_i = next(i for i, h in enumerate(header) if h == 'text')
for i,h in enumerate(header):
if h.startswith('contains_url') or h in ('has_url', 'has_url_') or h == 'has_url?':
print(f"column {i}: {h}")
PYRepository: SafeFam/SafeFam_AI Length of output: 365 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
echo "=== raw dataset head =="
python3 - "$csv" <<'PY'
import sys
path = sys.argv[1]
with open(path, encoding='utf-8-sig') as f:
for i in range(6):
line = next(f, None)
print(f"line[{i}] len={len(line)} text={next(f, '')!r}")
PY
echo "=== exact mismatch report against column names =="
python3 - "$csv" <<'PY'
import re, sys, glob
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
print("header=[{}]" .format(','.join(repr(h) for h in header)))
text_i = next(i for i, h in enumerate(header) if h == 'text')
has_url_i = next(i for i, h in enumerate(header) if h == 'has_url')
total = same = mism = 0
examples = []
for i in range(2, 6)
line = next(f, None)
print(i, line.strip()[:100], text_i, has_url_i)
PYRepository: SafeFam/SafeFam_AI Length of output: 1289 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
echo "=== exact mismatch report =="
python3 - "$csv" <<'PY'
import re, sys
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
text_i = header.index('text')
has_url_i = header.index('has_url')
total = same = mism = 0
examples = []
for row_no, row in enumerate(f, start=2):
fields = row.rstrip('\n').split(',', has_url_i + 1)
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
if declared == detected:
same += 1
else:
mism += 1
if len(examples) < 10:
examples.append((row_no, len(txt), declared, detected, txt[:120]))
print("rows", total)
print("same", same)
print("mismatches", mism)
print("mismatches_fraction", mism / total if total else 0)
for ex in examples:
print(ex)
PYRepository: SafeFam/SafeFam_AI Length of output: 301 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
python3 - "$csv" <<'PY'
import re, sys, pathlib
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
text_i = header.index('text')
has_url_i = header.index('has_url')
rows = []
for row_no, row in enumerate(f, start=2):
fields = row.rstrip('\n').split(',', has_url_i + 1)
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
rows.append((detected, declared))
same = sum(a == b for a, b in rows)
mismatches = [(detected, declared) for detected, declared in rows if detected != declared]
print("rows", len(rows))
print("same", same)
print("mismatches", len(mismatches))
print("mismatches_fraction", len(mismatches) / len(rows) if rows else 0)
for detected, declared in mismatches[:10]:
print("detected", detected, "declared", declared)
PYRepository: SafeFam/SafeFam_AI Length of output: 272 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
python3 - "$csv" <<'PY'
import re, sys, pathlib
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
text_i = header.index('text')
has_url_i = header.index('has_url')
rows = []
for row_no, row in enumerate(f, start=2):
fields = row.rstrip('\n').split(',', has_url_i + 1)
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
rows.append((row_no, detected, declared))
same = sum((detected == declared) for _, detected, declared in rows)
mismatches = [row for _, detected, declared in rows if detected != declared]
print("rows", len(rows))
print("same", same)
print("mismatches", len(mismatches))
print("mismatches_fraction", len(mismatches) / len(rows) if rows else 0)
for row_no, detected, declared in mismatches[:10]:
print({"row": row_no, "detected": detected, "declared": declared})
PYRepository: SafeFam/SafeFam_AI Length of output: 272 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
python3 <<'PY' "$csv"
import re, sys, pathlib
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
text_i = header.index('text')
has_url_i = header.index('has_url')
rows = []
for row_no, row in enumerate(f, start=2):
fields = row.rstrip('\n').split(',', has_url_i + 1)
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
rows.append((row_no, detected, declared))
same = sum((detected == declared) for _, detected, declared in rows)
mismatches = [row for _, detected, declared in rows if detected != declared]
print("rows", len(rows))
print("same", same)
print("mismatches", len(mismatches))
print("mismatches_fraction", len(mismatches) / len(rows) if rows else 0)
for row_no, detected, declared in mismatches[:10]:
print({"row": row_no, "detected": detected, "declared": declared})
PYRepository: SafeFam/SafeFam_AI Length of output: 395 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing total dataset 2705.csv' data_science/Data/SMSData | head -n 1 || true)"
python3 - "$csv" <<'PY'
import re, sys, pathlib
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
print("csv", path)
print("header", header)
text_i = header.index('text')
has_url_i = header.index('has_url')
rows = []
for row_no, row in enumerate(f, start=2):
fields = row.rstrip('\n').split(',', has_url_i + 1)
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
rows.append((row_no, len(txt), detected, declared))
same = sum((detected == declared) for _, detected, declared in rows)
mismatches = [row for _, detected, declared in rows if detected != declared]
print("rows", len(rows))
print("same", same)
print("mismatches", len(mismatches))
print("mismatches_fraction", len(mismatches) / len(rows) if rows else 0)
for row_no, txt_len, detected, declared in mismatches[:10]:
print({"row": row_no, "text_length": txt_len, "detected": detected, "declared": declared})
PYRepository: SafeFam/SafeFam_AI Length of output: 288 🏁 Script executed: #!/bin/bash
set -euo pipefail
csv="$(fd -t f 'phishing_total_dataset_2705.csv' . | head -n 1 || true)"
python3 - "$csv" <<'PY'
import re, sys, pathlib
path = sys.argv[1]
url_re = r"(?i)(?<!@)(?:https?://|www\.)[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+;=%]+"
p = re.compile(url_re)
with open(path, encoding='utf-8-sig') as f:
header = f.readline().strip().split(',')
print("csv", path)
print("header", header)
text_i = header.index('text')
has_url_i = header.index('has_url')
rows = []
for row_no, row in enumerate(f, start=2):
fields = row.rstrip('\n').split(',', has_url_i + 1)
txt = fields[text_i]
declared = bool(int(fields[has_url_i])) if fields[has_url_i] else False
detected = bool(p.search(txt))
rows.append((row_no, len(txt), detected, declared))
same = sum((detected == declared) for _, detected, declared in rows)
mismatches = [row for _, detected, declared in rows if detected != declared]
print("rows", len(rows))
print("same", same)
print("mismatches", len(mismatches))
print("mismatches_fraction", len(mismatches) / len(rows) if rows else 0)
for row_no, txt_len, detected, declared in mismatches[:10]:
print({"row": row_no, "text_length": txt_len, "detected": detected, "declared": declared})
PYRepository: SafeFam/SafeFam_AI Length of output: 390 Reproduce the same Training and batch inference pass 📍 Affects 2 files
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| X_text = _vectorizer.transform([text_norm]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| X = hstack([X_text, csr_matrix(struct)]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 기존 artifact가 기대하는 입력 구조를 유지 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_features = _vectorizer.transform([normalized_text]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| feature_matrix = hstack( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_features, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| csr_matrix(struct_features), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phishing_idx = _classes.index("phishing") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prob_phishing = _model.predict_proba(X)[0][phishing_idx] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| risk_score = int(prob_phishing * 100) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phishing_index = _classes.index("phishing") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phishing_probability = _model.predict_proba( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| feature_matrix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )[0][phishing_index] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"[NaiveBayes] 문자 분석 완료 - 위험도 점수: {risk_score}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| risk_score = int(phishing_probability * 100) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "[NaiveBayes] 문자 분석 완료 - 위험도 점수: %s", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| risk_score, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "engine": "naive_bayes", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "is_available": True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "result": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "grade": determine_text_risk_grade(risk_score), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "risk_score": risk_score, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "is_suspected_phishing": bool(prob_phishing >= _threshold), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_message": None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "is_suspected_phishing": bool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phishing_probability >= _threshold | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error_message": None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "[NaiveBayes] 추론 중 비정상 오류 발생. error_type=%s", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type(exception).__name__, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "engine": "naive_bayes", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "is_available": False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "result": dict(DEFAULT_ANALYSIS_RESULT, error_message="Inference Error") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "result": dict( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_ANALYSIS_RESULT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error_message="Inference Error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: SafeFam/SafeFam_AI
Length of output: 194
🏁 Script executed:
Repository: SafeFam/SafeFam_AI
Length of output: 14541
Copy the Kiwi tokenizer package into the runtime image.
data_science/SMSModel/tokenization/kiwi_tokenizer.pydefineskiwi_tokenize, and the vectorizer model was built with that callable. The Dockerfile only runsKIWIduring build, then copiesapp, the artifact files, andmodels, sodata_science/SMSModel/tokenization/__init__.pyanddata_science/SMSModel/tokenization/kiwi_tokenizer.pyare absent at runtime and model loading can fail withModuleNotFoundError.Copy the tokenization package into the image, or move
kiwi_tokenizeunder a path already included at runtime.🤖 Prompt for AI Agents