Skip to content
Merged
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 .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# split manifest의 SHA-256은 artifact 재현성 검증에 쓰이므로
# 체크아웃 플랫폼과 무관하게 항상 LF로 고정한다.
# (core.autocrlf=true 환경에서 CRLF로 변환되면 같은 split인데도 해시가 달라진다)
data_science/SMSModel/splits/*.csv text eol=lf
5 changes: 1 addition & 4 deletions app/analysis/institution/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ def _find_mentioned_institution(text: str) -> OfficialInstitution | None:
return None


# 문자에 언급된 기관명과 실제 링크된 URL의 도메인이 그 기관의 공식 도메인과 일치하는지 대조.
# 기관 언급 자체가 없으면 대조할 대상이 없으므로 checked=False로 스킵하지만, URL이 없어서
# 도메인 대조를 못 한 경우에도 감지된 기관명은 채워서 반환한다 - 발신번호-기관 연계(issue #67)
# 등 URL 유무와 무관하게 "이 문자가 어느 기관 명의였는지"가 필요한 소비자를 위함이다.
# 문자에 언급된 기관명과 실제 링크된 URL의 도메인이 그 기관의 공식 도메인과 일치하는지 대조
def analyze_institution_match(text: str, traced_url: str | None = None) -> dict:
no_institution_result = {
"checked": False,
Expand Down
15 changes: 5 additions & 10 deletions app/analysis/institution/registry.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
from dataclasses import dataclass, field


@dataclass(frozen=True)
class OfficialInstitution:
# 공식 기관명 (로그/응답 표시용)
name: str
# 문자에서 실제 쓰이는 표기 변형 (이 중 하나라도 본문에 있으면 해당 기관 언급으로 간주)
aliases: tuple[str, ...]
# 공식 도메인. 서브도메인도 인정하려면 상위 도메인만 등록 (예: "kbstar.com"이면 "obank.kbstar.com"도 매치)
official_domains: tuple[str, ...]
# 공식 대표번호(고객센터). 각 기관 공식 홈페이지에서 직접 확인해 채움 (아래 출처 주석 참고)
official_phone_numbers: tuple[str, ...] = field(default_factory=tuple)
# SMS 실제 발신번호는 기관마다 여러 개이고 수시로 바뀌며 공개된 단일 출처가 없어
# 신뢰할 만한 데이터를 확보하기 전까지 비워둔다 — 향후 실제 수신 데이터가 쌓이면 채울 것.
known_sender_numbers: tuple[str, ...] = field(default_factory=tuple)


Expand All @@ -22,6 +15,7 @@ class OfficialInstitution:
# 잘못된 공식 정보를 노출하면 오탐/오정보 제공으로 이어지므로, 향후 정보 추가/변경 시에도
# 반드시 공식 채널에서 직접 재확인할 것.
OFFICIAL_INSTITUTIONS: tuple[OfficialInstitution, ...] = (

# 은행
OfficialInstitution(
"국민은행", ("국민은행", "KB국민은행"), ("kbstar.com",), ("1588-9999", "1599-9999", "1644-9999")
Expand All @@ -43,21 +37,22 @@ class OfficialInstitution:
OfficialInstitution(
"우체국", ("우체국", "우정사업본부"), ("epost.go.kr", "koreapost.go.kr"), ("1588-1300",)
),

# 카드사
OfficialInstitution("신한카드", ("신한카드",), ("shinhancard.com",), ("1544-7000",)),
OfficialInstitution("삼성카드", ("삼성카드",), ("samsungcard.com",), ("1588-8700",)),
OfficialInstitution("현대카드", ("현대카드",), ("hyundaicard.com",), ("1577-6000",)),
OfficialInstitution("KB국민카드", ("KB국민카드", "국민카드"), ("kbcard.com",), ("1588-1688",)),
OfficialInstitution("롯데카드", ("롯데카드",), ("lottecard.co.kr",), ("1588-8100",)),
# 최초 작성 시 "uricard.com"으로 잘못 기재했던 것을 검증 과정에서 발견해 수정함
OfficialInstitution("우리카드", ("우리카드",), ("wooricard.com",), ("1588-9955", "1599-9955")),

# 공공/사법기관
OfficialInstitution("금융감독원", ("금융감독원",), ("fss.or.kr",), ("1332",)),
OfficialInstitution("금융위원회", ("금융위원회",), ("fsc.go.kr",), ("02-2100-2500",)),
OfficialInstitution("경찰청", ("경찰청",), ("police.go.kr",), ("182",)),
OfficialInstitution("검찰청", ("검찰청",), ("spo.go.kr",), ("1301",)),
# 홈택스(hometax.go.kr)는 국세청이 직접 운영하는 전자세정 서비스 도메인이라 함께 등록 —
# 실제로 nts.go.kr보다 홈택스 링크가 더 흔히 쓰이므로 빠지면 정상 링크가 오탐됨

# 홈택스(hometax.go.kr)는 국세청이 직접 운영하는 전자세정 서비스 도메인이라 함께 등록
OfficialInstitution("국세청", ("국세청",), ("nts.go.kr", "hometax.go.kr"), ("126",)),
OfficialInstitution("관세청", ("관세청",), ("customs.go.kr",), ("125",)),
OfficialInstitution(
Expand Down
19 changes: 18 additions & 1 deletion app/analysis/text/structural_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,20 @@
)


# 배열의 열 순서가 학습 및 추론에서 동일해야 하므로 상수로 고정합니다.
# 합법 광고 문자의 법정 표기
AD_DISCLOSURE_PATTERN = re.compile(
r"\(\s*광고\s*\)|\[\s*광고\s*\]|^광고",
re.IGNORECASE | re.MULTILINE,
)

OPT_OUT_PATTERN = re.compile(
r"수신\s*거부|무료거부|"
r"080[-.\s]?\d{3,4}[-.\s]?\d{4}",
re.IGNORECASE,
)


# 배열의 열 순서가 학습 및 추론에서 동일해야 하므로 상수로 고정
STACKING_STRUCTURAL_FEATURE_NAMES: tuple[str, ...] = (
"has_url",
"has_short_url",
Expand All @@ -63,6 +76,8 @@
"has_personal_info_request",
"has_link_action",
"is_long_text",
"has_ad_disclosure",
"has_opt_out",
)


Expand Down Expand Up @@ -113,6 +128,8 @@ def extract_stacking_structural_features(
bool(PERSONAL_INFO_PATTERN.search(text)),
bool(LINK_ACTION_PATTERN.search(text)),
len(text) > 100,
bool(AD_DISCLOSURE_PATTERN.search(text)),
bool(OPT_OUT_PATTERN.search(text)),
],
dtype=np.float64,
)
Expand Down
72 changes: 72 additions & 0 deletions data_science/SMSModel/artifacts/stacking/v3-baseline/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"artifact_version": "v3",
"created_at": "2026-08-18T06:07:16.622322+00:00",
"dataset": {
"dataset_fingerprint": "46aa236b5c70453bc5b5e91664f4a43d499fffd9a3f103eec9178a30aab85f22",
"holdout_rows": 280,
"total_csv_rows": 3078,
"training_pool_rows": 804
},
"dataset_path": "Data/SMSData/phishing_total_dataset_reclassified.csv",
"library_versions": {
"joblib": "1.5.3",
"numpy": "2.4.6",
"pandas": "2.3.3",
"python": "3.13.14",
"scikit_learn": "1.8.0",
"scipy": "1.18.0"
},
"model": {
"base_models": [
"naive_bayes",
"logistic_regression",
"linear_svm"
],
"meta_classifier": "LogisticRegression",
"meta_feature_names": [
"naive_bayes_score",
"logistic_regression_score",
"linear_svm_score",
"has_url",
"has_short_url",
"has_phone",
"has_account",
"has_card",
"has_amount",
"has_web_tag",
"has_urgency",
"has_transfer_request",
"has_personal_info_request",
"has_link_action",
"is_long_text"
],
"model_name": "stacking_phishing_classifier",
"oof_splits": 5,
"random_state": 42,
"threshold": 0.30448047609435736
},
"model_configuration_sha256": "702b42d87ebbecea9dab08a809a1a38d324a4653a44c7e8e98d095e0431159b4",
"model_sha256": "335ad70ab86b59fc55581e08060f70bc489ec5c1065c13c970ff3b87e717d481",
"schema_version": 2,
"split_manifest": "sms_split_v3.csv",
"split_manifest_sha256": "459c24d86bc73503147d8fbe0844b30ee985a0b2a1e8d4d3981510a124037d2a",
"splits": {
"test": 117,
"train": 565,
"validation": 122
},
"training_policy": {
"final_evaluation_split": "test",
"holdout_used_during_training": false,
"random_state": 42,
"test_used_for_tuning": false,
"threshold_selection_split": "validation",
"training_split": "train"
},
"validation": {
"f2": 0.9061488673139159,
"recall": 0.9655172413793104,
"target_recall": 0.95,
"target_recall_met": true
}
}
Binary file not shown.
14 changes: 8 additions & 6 deletions data_science/SMSModel/artifacts/stacking/v3/metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"artifact_version": "v3",
"created_at": "2026-08-18T06:07:16.622322+00:00",
"created_at": "2026-08-18T08:25:33.056102+00:00",
"dataset": {
"dataset_fingerprint": "46aa236b5c70453bc5b5e91664f4a43d499fffd9a3f103eec9178a30aab85f22",
"holdout_rows": 280,
Expand Down Expand Up @@ -38,15 +38,17 @@
"has_transfer_request",
"has_personal_info_request",
"has_link_action",
"is_long_text"
"is_long_text",
"has_ad_disclosure",
"has_opt_out"
],
"model_name": "stacking_phishing_classifier",
"oof_splits": 5,
"random_state": 42,
"threshold": 0.30448047609435736
"threshold": 0.3038914498086088
},
"model_configuration_sha256": "702b42d87ebbecea9dab08a809a1a38d324a4653a44c7e8e98d095e0431159b4",
"model_sha256": "335ad70ab86b59fc55581e08060f70bc489ec5c1065c13c970ff3b87e717d481",
"model_configuration_sha256": "8d96d4db57cf1bfc70b73ffa4bd27deaf2c0dbeff6be4c15b2b21d6fd9c5fcdd",
"model_sha256": "72cfe282beaba96e5c7c864ec3a2431e1f9ae41f25a2369a616abcbd6927886f",
"schema_version": 2,
"split_manifest": "sms_split_v3.csv",
"split_manifest_sha256": "459c24d86bc73503147d8fbe0844b30ee985a0b2a1e8d4d3981510a124037d2a",
Expand All @@ -64,7 +66,7 @@
"training_split": "train"
},
"validation": {
"f2": 0.9061488673139159,
"f2": 0.9427609427609428,
"recall": 0.9655172413793104,
"target_recall": 0.95,
"target_recall_met": true
Expand Down
Binary file modified data_science/SMSModel/artifacts/stacking/v3/model.joblib
Binary file not shown.
Loading
Loading