SeaWolf-AI's picture
feat: globalize ARTEX/agentic-attack narrative (remove KR-bank specifics) + latest deep blocks
90f8f5a verified
Raw History Blame Contribute Delete
71.3 kB
# -*- coding: utf-8 -*-
"""치토스(CHITOS) 인스턴트 스캔 — 공개 UI(얇은 클라이언트) + 전문 리포트 생성. 한/영 2개국어.
로직·채점은 비공개 엔진(치토스 서버)에서 수행 → 이 공개 소스엔 방법이 없습니다.
진단 문구는 엔진이 KO/EN 양쪽으로 반환 → 토글 시 재스캔 없이 즉시 전환.
2단계 인증: 회사/조직 도메인 메일로 가입한 HF 계정 로그인 → 이메일 도메인 일치 시 심화.
서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)."""
import os, html, json, tempfile, datetime
from urllib.parse import urlparse
import requests
import gradio as gr
from huggingface_hub import whoami
from gradio_client import Client
ENGINE = os.environ.get("CHITOS_ENGINE_REPO", "")
HFTOKEN = os.environ.get("HF_TOKEN", "")
CONSULT = os.environ.get("CHITOS_CONSULT_ENDPOINT", "")
_eng_client = None
GCOL = {"A+": "#067647", "A": "#067647", "B": "#2563eb", "C": "#b54708", "D": "#b54708", "F": "#b42318", "-": "#9ca3af"}
SEV = {"hi": ("#b42318", "#fef3f2"), "mid": ("#b54708", "#fffaeb"), "low": ("#8a6d00", "#fdfce8")}
SEVLAB = {"ko": {"hi": "높음", "mid": "보완", "low": "낮음"}, "en": {"hi": "High", "mid": "Advisory", "low": "Low"}}
def _L(ko, en, lang):
return ko if lang == "ko" else en
def _P(v, lang):
"""엔진이 [ko,en]로 준 값에서 언어 선택. 문자열이면 그대로."""
if isinstance(v, (list, tuple)) and len(v) == 2 and all(isinstance(e, str) for e in v):
return v[0] if lang == "ko" else v[1]
return str(v)
def _engine():
global _eng_client
if _eng_client is None and ENGINE:
try:
_eng_client = Client(ENGINE, token=HFTOKEN or None)
except TypeError:
_eng_client = Client(ENGINE, hf_token=HFTOKEN or None)
return _eng_client
def _api(path, payload):
global _eng_client
import time as _t
last = ""
for attempt in range(4):
c = _engine()
if c is None:
return {"error": ["진단 엔진이 아직 연결되지 않았습니다(운영 설정 대기).", "Diagnosis engine not connected yet (operational setup pending)."]}
try:
res = c.predict(payload.get("url", ""), api_name="/" + path.strip("/"))
return json.loads(res)
except Exception as e:
last = str(e)
transient = any(k in last for k in ("429", "Too Many", "503", "502", "504",
"timed out", "timeout", "Connection", "starting"))
if transient and attempt < 3:
_eng_client = None
_t.sleep(2 + 3 * attempt)
continue
break
return {"error": ["진단 엔진이 잠시 혼잡합니다. 10~20초 후 다시 눌러 주세요. (%s)" % last[:70],
"Engine is briefly busy. Please try again in 10–20s. (%s)" % last[:70]]}
def _domain(u):
u = (u or "").strip()
if "://" not in u:
u = "https://" + u
return (urlparse(u).hostname or "").replace("www.", "")
# ===== 비주얼 조각 =====
def _letter(p):
return "A+" if p >= 93 else "A" if p >= 85 else "B" if p >= 75 else "C" if p >= 65 else "D" if p >= 50 else "F"
def _overall(d):
sec = d.get("sec_grade", ["-", 0])[1] or 0
seo = d.get("seo_grade", ["-", 0])[1] or 0
geo = d.get("geo_grade", ["-", 0])[1] or 0
pct = round(sec * 0.45 + geo * 0.30 + seo * 0.25)
return pct, _letter(pct)
def _ring(pct, grade, lang, dark=False):
col = GCOL.get(grade, "#6d28d9")
inner = "#1b0f3b" if dark else "#fff"
sub = "#d6bcff" if dark else "#6b7280"
return ('<div style="width:138px;height:138px;border-radius:50%%;flex:none;background:conic-gradient(%s %s%%,%s 0);'
'display:flex;align-items:center;justify-content:center;box-shadow:0 6px 20px rgba(109,40,217,.18)">'
'<div style="width:106px;height:106px;border-radius:50%%;background:%s;display:flex;flex-direction:column;align-items:center;justify-content:center">'
'<div style="font-size:36px;font-weight:900;color:%s;line-height:1">%s</div>'
'<div style="font-size:12px;color:%s;margin-top:2px">%s/100</div>'
'<div style="font-size:9.5px;letter-spacing:2px;color:%s">%s</div></div></div>') % (
col, pct, ("#2a1a52" if dark else "#e9e4f7"), inner, col, grade, sub, pct, sub, _L("종합", "OVERALL", lang))
def _scorebar(label, g):
L, s = g[0], g[1] or 0
col = GCOL.get(L, "#6d28d9")
return ('<div style="display:flex;align-items:center;gap:10px;margin:8px 0">'
'<div style="width:110px;font-size:12.5px;color:#374151;font-weight:700;flex:none">%s</div>'
'<div style="flex:1;height:13px;background:#eef1f6;border-radius:999px;overflow:hidden">'
'<div style="height:100%%;width:%s%%;background:linear-gradient(90deg,%s,%s99);border-radius:999px"></div></div>'
'<div style="width:70px;text-align:right;font-size:12.5px;color:%s;font-weight:800;flex:none">%s·%s</div></div>') % (
label, s, col, col, col, L, s)
def _locks(n, lang):
cells = ""
for i in range(6):
on = i < n
cells += ('<div style="flex:1;text-align:center;border:1px solid #e4e9ef;border-radius:8px;padding:7px 2px;background:%s">'
'<div style="font-size:18px">%s</div></div>') % ("#ecfdf3" if on else "#fef3f2", "🔒" if on else "🔓")
cap = _L("보안 응답 헤더 6종 중 <b>%d개</b> 적용", "<b>%d of 6</b> security response headers applied", lang) % n
return ('<div style="font-size:12px;color:#4b5563;margin:10px 0 4px">🔐 ' + cap + '</div>'
'<div style="display:flex;gap:5px">' + cells + '</div>')
def _metacards(meta, lang):
tl = len(html.unescape(str(meta.get("title") or "")))
data = [(_L("제목 길이", "Title len", lang), "%d%s" % (tl, _L("자", "ch", lang))),
("H1", str(meta.get("h1", 0))),
(_L("구조화", "Schema", lang), str(meta.get("jsonld", 0))),
(_L("홈 용량", "Home", lang), "%dKB" % meta.get("size_kb", 0)),
("alt", "%d%%" % meta.get("alt_cov", 0))]
cells = "".join('<div style="flex:1;text-align:center;border:1px solid #e4e9ef;border-radius:10px;padding:8px 4px">'
'<div style="font-size:17px;font-weight:800;color:#2e1065">%s</div>'
'<div style="font-size:10.5px;color:#6b7280;margin-top:2px">%s</div></div>' % (v, k) for k, v in data)
return '<div style="display:flex;gap:6px;margin:8px 0">' + cells + '</div>'
def _consult_cta(nhi, nmid, nlow, lang):
total = nhi + nmid + nlow
if lang == "ko":
risk = '<b style="color:#b42318">%d</b>개 개선점(높음 %d · 보완 %d · 낮음 %d)' % (total, nhi, nmid, nlow)
body = ("이 진단에서 " + risk + "이 확인됐습니다. 하지만 <b>비침투 스캔은 ‘밖에서 보이는 표면’까지</b>입니다 — "
"SQL삽입·파일업로드·인증우회·IDOR 같은 <b>실제 취약점</b>, 그리고 <b>ARTEX</b>처럼 AI 에이전트가 자동으로 뚫는 위협은 "
"<b>능동 정밀 진단</b>으로만 확인됩니다.")
areas = "웹서비스 · AX 인프라 · AI 모델 · AI 에이전트(AX-RAY) 4대 진단 + 금융보안원 17항목 레드티밍"
go = "👉 <b>③ 정밀 진단 상담</b> 탭에서 분야를 선택해 신청하세요. (서면 허가 기반·국내외 준거 수용)"
head = "🎯 다음 단계 — 정밀 진단 상담"
else:
risk = '<b style="color:#b42318">%d</b> improvement points (High %d · Advisory %d · Low %d)' % (total, nhi, nmid, nlow)
body = ("This scan found " + risk + ". But a <b>non-intrusive scan only shows the surface</b> — "
"<b>real vulnerabilities</b> like SQLi, file upload, auth bypass and IDOR, and threats where an AI agent breaks in automatically (as with <b>ARTEX</b>), "
"are confirmed only by <b>active in-depth testing</b>.")
areas = "Web Service · AX Infra · AI Model · AI Agent (AX-RAY) — 4 diagnoses + FSI 17-item red-teaming"
go = "👉 Request it in the <b>③ In-Depth Consultation</b> tab (authorization-based · aligned to domestic & international standards)."
head = "🎯 Next step — in-depth consultation"
return ('<div class="chdk" style="background:linear-gradient(135deg,#4c1d95,#7c3aed);border-radius:16px;padding:18px 20px;margin-top:18px;color:#f3e8ff">'
'<div style="font-size:16px;font-weight:800;color:#fff">' + head + '</div>'
'<div style="font-size:13px;margin-top:8px;line-height:1.55">' + body + '</div>'
'<div style="background:#ffffff1f;border-radius:10px;padding:9px 13px;margin-top:10px;font-size:12.5px;color:#fff">🛡 ' + areas + '</div>'
'<div style="font-size:13px;margin-top:10px;font-weight:700;color:#fff">' + go + '</div></div>')
def _upsell(lang):
return ('<div class="chdk" style="background:linear-gradient(135deg,#6d28d9,#8b5cf6);border-radius:14px;padding:16px 18px;margin:16px 0">'
'<div style="font-weight:800;font-size:15px;color:#fff">🔓 ' + _L("전사이트 정밀 리포트 — ② 도메인 인증 탭", "Full-site report — ② Domain Verification tab", lang) + '</div>'
'<div style="font-size:13px;color:#f3e8ff;margin-top:6px">' + _L(
"이 리포트는 <b>홈 1페이지</b> 기준입니다. 회사/조직 도메인 메일로 가입한 Hugging Face 계정으로 로그인하면 — <b>전사이트 페이지별 이슈 · 구조화데이터 커버리지 · 이메일 위조방지(SPF/DMARC) · 자기모순(주소·전화) 탐지 · 다운로드 리포트</b>까지 받습니다.",
"This report is based on the <b>home page</b>. Sign in with a Hugging Face account on your company/org domain email to unlock — <b>per-page issues across the whole site · structured-data coverage · email anti-spoofing (SPF/DMARC) · self-contradiction (address/phone) detection · downloadable report</b>.", lang)
+ '</div></div>')
def _statrow(label, ok, val="", warn=False):
col = "#16a34a" if ok else ("#b54708" if warn else "#dc2626")
sym = "✓" if ok else ("!" if warn else "✕")
v = ('<span style="color:#6b7280;font-size:11.5px;margin-left:6px">%s</span>' % html.escape(str(val))) if val else ""
return ('<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid #eef1f6;border-radius:8px">'
'<span style="color:%s;font-weight:900;width:13px;text-align:center;flex:none">%s</span>'
'<span style="font-size:12.5px;color:#374151;flex:1">%s</span>%s</div>') % (col, sym, html.escape(label), v)
def _email_block(ed, es, lang):
if not ed:
return ('<div style="display:flex;gap:6px">'
+ _statrow("SPF", bool(es.get("spf")), warn=not es.get("spf"))
+ _statrow("DMARC", bool(es.get("dmarc")), warn=not es.get("dmarc")) + '</div>')
spf_ok = ed.get("spf") and ed.get("spf_qual") == "-all"
dmarc_ok = ed.get("dmarc") and ed.get("dmarc_p") != "none"
rows = [
_statrow(_L("SPF (발신 서버 인증)", "SPF (sender authorization)", lang), ed.get("spf") and ed.get("spf_qual") != "+all", _L("종료규칙 ", "qualifier ", lang) + str(ed.get("spf_qual", "?")), warn=(ed.get("spf") and not spf_ok)),
_statrow(_L("SPF DNS 룩업 (≤10)", "SPF DNS lookups (≤10)", lang), ed.get("spf_lookups", 0) <= 10, "~%d" % ed.get("spf_lookups", 0), warn=(ed.get("spf_lookups", 0) > 10)),
_statrow(_L("DKIM 서명", "DKIM signing", lang), ed.get("dkim", False), warn=not ed.get("dkim", False)),
_statrow(_L("DMARC 정책", "DMARC policy", lang), dmarc_ok, "p=" + str(ed.get("dmarc_p", "none")), warn=(ed.get("dmarc") and not dmarc_ok)),
_statrow(_L("BIMI (브랜드 로고)", "BIMI (brand logo)", lang), ed.get("bimi", False), warn=not ed.get("bimi", False)),
_statrow(_L("MTA-STS (전송 암호화 강제)", "MTA-STS (enforced mail TLS)", lang), ed.get("mta_sts", False), warn=not ed.get("mta_sts", False)),
_statrow(_L("TLS-RPT (메일 TLS 리포트)", "TLS-RPT (mail TLS reporting)", lang), ed.get("tls_rpt", False), warn=not ed.get("tls_rpt", False)),
]
return '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">' + "".join(rows) + '</div>'
def _dns_block(ed, lang):
if not ed:
return "—"
rows = [
_statrow("DNSSEC", ed.get("dnssec", False), warn=not ed.get("dnssec", False)),
_statrow(_L("CAA (인증서 발급 통제)", "CAA (cert issuance control)", lang), ed.get("caa", False), warn=not ed.get("caa", False)),
_statrow(_L("MX (메일 수신)", "MX (mail receiving)", lang), ed.get("mx", 0) > 0, "%d" % ed.get("mx", 0)),
_statrow(_L("NS 이중화 (2+)", "NS redundancy (2+)", lang), ed.get("ns", 0) >= 2, "%d" % ed.get("ns", 0), warn=(ed.get("ns", 0) < 2)),
]
return '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">' + "".join(rows) + '</div>'
def _sitesec_block(ss, extra_exposed, lang):
if not ss:
return "—"
n = ss.get("pages", 0) or 1
sri_t = ss.get("sri_total", 0)
rows = [
_statrow(_L("전 페이지 HTTPS", "HTTPS on all pages", lang), ss.get("https_pages", 0) == n, "%d/%d" % (ss.get("https_pages", 0), n)),
_statrow(_L("핵심 보안헤더 적용", "Core security headers", lang), ss.get("missing_hdr_pages", 0) == 0, _L("누락 ", "missing ", lang) + "%d" % ss.get("missing_hdr_pages", 0), warn=ss.get("missing_hdr_pages", 0) > 0),
_statrow(_L("혼합 콘텐츠(HTTP 리소스) 없음", "No mixed content (HTTP resources)", lang), ss.get("mixed_pages", 0) == 0, _L("%d페이지", "%d pages", lang) % ss.get("mixed_pages", 0) if ss.get("mixed_pages", 0) else ""),
_statrow(_L("외부 스크립트 SRI", "External-script SRI", lang), sri_t == 0 or ss.get("sri_with", 0) == sri_t, "%d/%d" % (ss.get("sri_with", 0), sri_t), warn=(sri_t and ss.get("sri_with", 0) < sri_t)),
_statrow(_L("민감정보 패턴 비노출", "No PII patterns exposed", lang), ss.get("pii_pages", 0) == 0, _L("%d페이지", "%d pages", lang) % ss.get("pii_pages", 0) if ss.get("pii_pages", 0) else ""),
_statrow(_L("추가 민감 파일 비노출", "No extra sensitive files", lang), not extra_exposed, (", ".join(extra_exposed) if extra_exposed else "")),
]
ext = ss.get("ext_scripts", [])
extd = ""
if ext:
chips = "".join('<span style="background:#f5f3ff;color:#5b21b6;border-radius:6px;padding:2px 8px;font-size:11px;margin:2px;display:inline-block">%s</span>' % html.escape(x) for x in ext)
extd = '<div style="font-size:11.5px;color:#6b7280;margin-top:6px">' + _L("외부 스크립트 출처: ", "Third-party script sources: ", lang) + '</div>' + chips
return '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">' + "".join(rows) + '</div>' + extd
def _checklist(items, lang):
if not items:
return ""
ic = {"pass": ("✓", "#16a34a", "#f0fdf4"), "warn": ("!", "#b54708", "#fffaeb"), "fail": ("✕", "#b42318", "#fef2f2")}
npass = len([i for i in items if i[0] == "pass"])
cells = ""
for st, lab in items:
sym, col, bg = ic.get(st, ic["warn"])
cells += ('<div style="display:flex;gap:8px;align-items:center;padding:5px 9px;border:1px solid #eef1f6;border-radius:7px;background:%s">'
'<span style="color:%s;font-weight:900;width:13px;text-align:center;flex:none">%s</span>'
'<span style="font-size:12px;color:#374151">%s</span></div>') % (bg, col, sym, html.escape(_P(lab, lang)))
head = (_L("점검 항목 전체", "All checks", lang) + ' · <b style="color:#16a34a">%d/%d %s</b>' % (npass, len(items), _L("통과", "pass", lang)))
return ('<div style="font-size:11.5px;color:#6b7280;margin:8px 0 5px">📋 ' + head + '</div>'
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:6px">' + cells + '</div>')
def _cards(items, empty, lang):
if not items:
return '<div style="color:#067647;font-weight:700;padding:8px 0">%s</div>' % empty
out = ""
for it in items:
sev = it[0]
title = _P(it[1], lang)
detail = _P(it[2], lang)
c, bg = SEV.get(sev, SEV["low"])
lab = SEVLAB[lang][sev]
out += ('<div style="border:1px solid #e4e9ef;border-left:4px solid %s;border-radius:9px;padding:10px 13px;margin:7px 0;background:%s">'
'<b style="color:%s">[%s] %s</b><div style="color:#4b5563;font-size:13px;margin-top:3px">%s</div></div>') % (
c, bg, c, lab, html.escape(title), html.escape(detail))
return out
def _traffic(matrix, lang):
rows = ""
for name, code in (matrix or {}).items():
ok = code in ("200", "302", "301")
blocked = code in ("403", "401", "429", "차단")
dot = "#16a34a" if ok else ("#dc2626" if blocked else "#9ca3af")
txt = _L("정상", "OK", lang) if ok else (_L("차단", "Blocked", lang) if blocked else code)
rows += ('<div style="display:flex;align-items:center;gap:9px;padding:6px 10px;border:1px solid #eef1f6;border-radius:8px">'
'<span style="width:11px;height:11px;border-radius:50%%;background:%s;flex:none"></span>'
'<span style="flex:1;font-size:12.5px">%s</span><span style="font-size:11.5px;color:#6b7280">%s</span></div>') % (dot, html.escape(str(name)), txt)
return '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:7px">%s</div>' % rows
def _note(msg, sev="ok"):
c = {"ok": "#067647", "mid": "#b54708", "hi": "#b42318"}.get(sev, "#067647")
bg = {"ok": "#ecfdf3", "mid": "#fffaeb", "hi": "#fef3f2"}.get(sev, "#ecfdf3")
return '<div style="border:1px solid #e4e9ef;border-left:4px solid %s;background:%s;border-radius:9px;padding:11px 14px;font-family:sans-serif;color:#374151">%s</div>' % (c, bg, msg)
def _h2(t):
return '<h2 style="font-size:18px;color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:7px;margin:24px 0 12px">%s</h2>' % t
def _priority(allf, lang):
def li(lst, when, col):
if not lst:
return ""
items = "".join("<li style='margin:3px 0'>%s</li>" % html.escape(_P(f[1], lang)) for f in lst)
return ('<div style="border:1px solid #e4e9ef;border-left:4px solid %s;border-radius:9px;padding:10px 14px;margin:7px 0">'
'<b style="color:%s">%s</b><ul style="margin:6px 0 0 2px;padding-left:18px;font-size:13px;color:#374151">%s</ul></div>') % (col, col, when, items)
hi = [f for f in allf if f[0] == "hi"]
mid = [f for f in allf if f[0] == "mid"]
low = [f for f in allf if f[0] == "low"]
out = (li(hi, _L("즉시 (높음)", "Immediate (High)", lang), "#b42318")
+ li(mid, _L("단기 (보완)", "Short-term (Advisory)", lang), "#b54708")
+ li(low, _L("중기 (낮음)", "Mid-term (Low)", lang), "#8a6d00"))
return out or ("<div style='color:#067647'>%s</div>" % _L("✓ 시급 조치 없음", "✓ No urgent action needed", lang))
def _badges_labels(lang):
return (_L("🛡 보안", "🛡 Security", lang), _L("📈 SEO", "📈 SEO", lang), _L("🔍 AI가시성", "🔍 AI Visibility", lang))
def build_teaser(d, lang="ko"):
if d.get("error"):
return _note("⚠ " + html.escape(_P(d["error"], lang)), "hi")
if d.get("blocked"):
return ('<div style="font-family:sans-serif;max-width:720px;margin:0 auto;border:1px solid #e4e9ef;border-radius:12px;padding:20px">'
'<h3 style="color:#067647">🛡 %s</h3><p>%s</p></div>') % (
_L("자동 공격 차단(WAF) 작동 중", "Automated attack blocking (WAF) active", lang),
_L("비정상 접근을 탐지해 차단했습니다. <b>방어가 잘 되어 있다는 신호</b>입니다. 치토스는 차단을 우회하지 않습니다.",
"Abnormal access was detected and blocked. <b>A sign your defenses work.</b> CHITOS does not bypass blocks.", lang))
host = d.get("host", "")
today = datetime.date.today().isoformat()
allf = list(d.get("sec", [])) + list(d.get("seo", [])) + list(d.get("geo", []))
nhi = len([f for f in allf if f[0] == "hi"])
nmid = len([f for f in allf if f[0] == "mid"])
top = [f for f in allf if f[0] == "hi"] + [f for f in allf if f[0] == "mid"]
top = top[:3]
topcards = _cards(top, _L("✓ 눈에 띄는 시급 이슈 없음", "✓ No notable urgent issues", lang), lang)
pct, grade = _overall(d)
mx = d.get("matrix", {})
ai_ok = len([1 for c in mx.values() if c in ("200", "301", "302")])
ai_tot = len(mx) or 9
bS, bSEO, bAI = _badges_labels(lang)
return ('<div style="font-family:sans-serif;max-width:720px;margin:0 auto">'
'<div style="display:flex;gap:18px;align-items:center;background:linear-gradient(135deg,#2e1065,#4c1d95 55%,#7c3aed 160%);border-radius:18px;padding:20px 22px;color:#fff">'
+ _ring(pct, grade, lang, dark=True)
+ '<div style="flex:1">'
'<div style="font-size:11px;letter-spacing:4px;color:#d6bcff;font-weight:800">C H I T O S · ' + _L("무료 즉시 진단", "Free Instant Scan", lang) + '</div>'
'<div style="font-size:20px;font-weight:800;margin:4px 0 2px;color:#fff">' + html.escape(host) + '</div>'
'<div style="font-size:12px;color:#e9d5ff">' + today + ' · ' + _L("홈 1페이지 · 비침투(읽기전용)", "home page · non-intrusive (read-only)", lang) + '</div>'
'<div style="display:flex;gap:7px;margin-top:10px;flex-wrap:wrap">'
'<span style="background:#ffffff33;color:#fff;border-radius:999px;padding:3px 11px;font-size:11.5px">🚨 ' + _L("높음", "High", lang) + ' ' + str(nhi) + '</span>'
'<span style="background:#ffffff33;color:#fff;border-radius:999px;padding:3px 11px;font-size:11.5px">⚠ ' + _L("보완", "Advisory", lang) + ' ' + str(nmid) + '</span>'
'<span style="background:#ffffff33;color:#fff;border-radius:999px;padding:3px 11px;font-size:11.5px">🤖 ' + _L("AI로봇", "AI bots", lang) + ' ' + str(ai_ok) + '/' + str(ai_tot) + '</span>'
'</div></div></div>'
+ '<div style="border:1px solid #e9e4f7;border-radius:14px;padding:14px 18px;margin-top:12px;background:#faf8ff">'
+ _scorebar(bS, d["sec_grade"])
+ _scorebar(bSEO, d.get("seo_grade", ["-", 0]))
+ _scorebar(bAI, d["geo_grade"])
+ '<div style="font-size:11px;color:#9ca3af;margin-top:4px">' + _L("종합은 보안 45% · AI가시성 30% · SEO 25% 가중 평균", "Overall = Security 45% · AI Visibility 30% · SEO 25% weighted", lang) + '</div></div>'
+ '<h3 style="color:#b42318;font-size:15px;margin:18px 0 6px">⚡ ' + _L("가장 급한 것 (상위 ", "Top priorities (top ", lang) + str(len(top)) + ')</h3>' + topcards
+ '<h3 style="color:#2e1065;font-size:15px;margin:18px 0 6px">🤖 ' + _L("AI 로봇이 내 사이트에 들어올 수 있나", "Can AI crawlers reach your site?", lang) + '</h3>'
+ _traffic(mx, lang)
+ '<div style="background:linear-gradient(135deg,#6d28d9,#8b5cf6);color:#fff;border-radius:14px;padding:16px 18px;margin-top:18px">'
'<div style="font-weight:800;font-size:15px;color:#fff">🔓 ' + _L("전사이트 정밀 리포트가 더 있습니다", "A full-site detailed report is available", lang) + '</div>'
'<div style="font-size:13px;color:#f3e8ff;margin-top:6px">' + _L(
"지금은 <b>홈 1페이지 요약</b>입니다. <b>② 도메인 인증 탭</b>에서 회사/조직 도메인 메일로 가입한 Hugging Face 계정으로 로그인하면 — 전사이트 페이지별 이슈 · 구조화데이터 커버리지 · <b>이메일 위조방지(SPF/DMARC)</b> · <b>자기모순(주소·전화) 탐지</b> · 조치 우선순위 · <b>다운로드 리포트</b>까지 받습니다.",
"This is a <b>home-page summary</b>. In the <b>② Domain Verification tab</b>, sign in with a Hugging Face account registered on your company/org domain email to unlock — per-page issues across the site · structured-data coverage · <b>email anti-spoofing (SPF/DMARC)</b> · <b>self-contradiction (address/phone) detection</b> · action priorities · <b>downloadable report</b>.", lang)
+ '</div></div>'
'<div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">' + _L("서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)", "Service: QuantID · Engine: CHITOS", lang) + '</div></div>')
def build_report(d, deep=False, verified_email=None, lang="ko", show_upsell=False):
if d.get("error"):
return _note("⚠ " + html.escape(_P(d["error"], lang)), "hi")
if d.get("blocked"):
return ('<div style="font-family:sans-serif;max-width:860px;margin:0 auto;border:1px solid #e4e9ef;border-radius:12px;padding:20px">'
'<h3 style="color:#067647">🛡 %s</h3><p>%s</p></div>') % (
_L("자동 공격 차단(WAF) 작동 중", "Automated attack blocking (WAF) active", lang),
_L("비정상 접근을 탐지해 차단했습니다. <b>방어가 잘 되어 있다는 좋은 신호</b>입니다. 치토스는 차단을 우회하지 않습니다(읽기전용).",
"Abnormal access was detected and blocked. <b>A good sign your defenses work.</b> CHITOS does not bypass blocks (read-only).", lang))
host = d.get("host", "")
meta = d.get("meta", {})
today = datetime.date.today().isoformat()
sec, seo, geo = d.get("sec", []), d.get("seo", []), d.get("geo", [])
allf = list(sec) + list(seo) + list(geo)
dp = d.get("deep", {}) if deep else {}
if dp:
allf += dp.get("issues", [])
nhi = len([f for f in allf if f[0] == "hi"])
nmid = len([f for f in allf if f[0] == "mid"])
nlow = len([f for f in allf if f[0] == "low"])
good = "".join('<span style="display:inline-block;background:#ecfdf3;color:#067647;border-radius:999px;padding:3px 11px;font-size:12px;margin:3px 4px 0 0">✓ %s</span>' % html.escape(_P(g, lang)) for g in d.get("good", [])) or "—"
bS, bSEO, bAI = _badges_labels(lang)
deep_html = ""
if dp:
es = dp.get("email_sec", {})
def _pill(ok, t):
c = "#16a34a" if ok else "#dc2626"
return '<span style="background:%s22;color:%s;border-radius:999px;padding:3px 11px;font-size:12px;font-weight:700">%s %s</span>' % (c, c, ("✓" if ok else "✕"), t)
sch = "".join('<span style="background:#ecfdf3;color:#067647;border-radius:6px;padding:2px 8px;font-size:11.5px;margin:2px;display:inline-block">%s</span>' % html.escape(x) for x in dp.get("schema_present", [])) or _L("없음", "none", lang)
schm = "".join('<span style="background:#fef3f2;color:#b42318;border-radius:6px;padding:2px 8px;font-size:11.5px;margin:2px;display:inline-block">%s</span>' % html.escape(x) for x in dp.get("schema_missing", [])) or "—"
incon = ""
if len(dp.get("phones", [])) > 1 or len(dp.get("addrs", [])) > 1 or len(dp.get("bizno", [])) > 1:
r = ""
if len(dp.get("phones", [])) > 1:
r += '<tr><td style="border:1px solid #e4e9ef;padding:6px">%s</td><td style="border:1px solid #e4e9ef;padding:6px">%s</td></tr>' % (_L("전화", "Phone", lang), html.escape(", ".join(dp["phones"])))
if len(dp.get("addrs", [])) > 1:
r += '<tr><td style="border:1px solid #e4e9ef;padding:6px">%s</td><td style="border:1px solid #e4e9ef;padding:6px">%s</td></tr>' % (_L("주소", "Address", lang), html.escape(" / ".join(dp["addrs"])))
if len(dp.get("bizno", [])) > 1:
r += '<tr><td style="border:1px solid #e4e9ef;padding:6px">%s</td><td style="border:1px solid #e4e9ef;padding:6px">%s</td></tr>' % (_L("사업자번호", "Biz reg. no.", lang), html.escape(", ".join(dp["bizno"])))
incon = ('<h4 style="color:#b54708;margin-top:14px">⚠ %s</h4><table style="width:100%%;border-collapse:collapse;font-size:12.5px">'
'<tr><th style="text-align:left;border:1px solid #e4e9ef;padding:6px;background:#f5f8fc">%s</th><th style="text-align:left;border:1px solid #e4e9ef;padding:6px;background:#f5f8fc">%s</th></tr>%s</table>') % (
_L("확인 필요 — 사이트 안에서 다르게 보이는 값", "Please verify — values that differ across the site", lang),
_L("항목", "Field", lang), _L("발견 값", "Found values", lang), r)
pglist = "".join('<div style="font-size:11.5px;color:#4b5563;padding:3px 0;border-bottom:1px dashed #eef1f6">%s %s <span style="color:#9ca3af">%s</span></div>' % (('✓' if c.get('meta') else '⚠'), html.escape(html.unescape(c.get('title') or c.get('url', ''))), html.escape(c.get('url', ''))) for c in dp.get("crawled", [])[:12])
deep_html = (
'<div style="background:#ede9fe;border-radius:10px;padding:10px 14px;margin-top:4px;color:#5b21b6;font-weight:700">🔓 ' + (_L("소유 인증 전용 심화 — 전사이트 %d페이지 정밀 분석 (비침투)", "Verified-owner deep scan — %d pages analyzed (non-intrusive)", lang) % dp.get("pages", 0)) + '</div>'
+ _h2("🔎 " + _L("심화 발견", "Deep findings", lang)) + _cards(dp.get("issues", []), _L("✓ 심화 점검 추가 이슈 없음", "✓ No additional deep-scan issues", lang), lang)
+ _h2("📊 " + _L("전사이트 요약", "Full-site summary", lang))
+ ('<table style="width:100%%;border-collapse:collapse;font-size:12.5px">'
'<tr><td style="border:1px solid #e4e9ef;padding:7px">%s</td><td style="border:1px solid #e4e9ef;padding:7px"><b>%d</b></td><td style="border:1px solid #e4e9ef;padding:7px">%s</td><td style="border:1px solid #e4e9ef;padding:7px"><b>%d</b></td></tr>'
'<tr><td style="border:1px solid #e4e9ef;padding:7px">%s</td><td style="border:1px solid #e4e9ef;padding:7px"><b>%d</b></td><td style="border:1px solid #e4e9ef;padding:7px">%s</td><td style="border:1px solid #e4e9ef;padding:7px"><b>%d</b></td></tr>'
'<tr><td style="border:1px solid #e4e9ef;padding:7px">%s</td><td style="border:1px solid #e4e9ef;padding:7px"><b>%d</b></td><td style="border:1px solid #e4e9ef;padding:7px">%s</td><td style="border:1px solid #e4e9ef;padding:7px"><b>%d</b></td></tr></table>') % (
_L("점검 페이지", "Pages scanned", lang), dp.get("pages", 0), _L("제목 중복", "Duplicate titles", lang), dp.get("dup_titles", 0),
_L("메타설명 없음", "No meta desc.", lang), dp.get("no_meta", 0), _L("얇은 페이지", "Thin pages", lang), dp.get("thin", 0),
_L("색인차단", "noindex", lang), dp.get("noindex", 0), _L("canonical 없음", "No canonical", lang), dp.get("no_canonical", 0))
+ _h2("🧩 " + _L("구조화 데이터 커버리지", "Structured-data coverage", lang)) + '<div style="font-size:12.5px"><b style="color:#067647">' + _L("보유:", "Present:", lang) + '</b> ' + sch + '<br><b style="color:#b42318;display:inline-block;margin-top:6px">' + _L("누락:", "Missing:", lang) + '</b> ' + schm + '</div>'
+ _h2("📧 " + _L("이메일 보안 심화 (SPF·DKIM·DMARC·BIMI)", "Email security — deep (SPF·DKIM·DMARC·BIMI)", lang)) + _email_block(dp.get("email_dns", {}), es, lang)
+ _h2("🌐 " + _L("DNS·도메인 심화", "DNS & domain — deep", lang)) + _dns_block(dp.get("email_dns", {}), lang)
+ _h2("🧱 " + _L("전사이트 보안 일관성", "Site-wide security consistency", lang)) + _sitesec_block(dp.get("site_sec", {}), dp.get("extra_exposed", []), lang)
+ incon
+ _h2("📋 " + _L("전사이트 점검 체크리스트", "Full-site checklist", lang)) + _checklist(dp.get("checks", []), lang)
+ _h2("🗂 " + (_L("점검 페이지 (%d)", "Scanned pages (%d)", lang) % dp.get("pages", 0))) + '<div style="max-height:220px;overflow:auto;border:1px solid #eef1f6;border-radius:8px;padding:8px 12px">' + pglist + '</div>'
)
vbanner = ('<div style="background:#ecfdf3;border:1px solid #a7f3d0;border-radius:10px;padding:10px 14px;color:#065f46;font-size:13px;margin-bottom:10px">✓ %s</div>' % (
_L("도메인 소유 인증 완료(<b>%s</b>) — 심화 진단 포함", "Domain ownership verified (<b>%s</b>) — deep scan included", lang) % html.escape(verified_email))) if verified_email else ""
kind = _L("심화 진단", "Deep Scan", lang) if deep else _L("인스턴트 진단", "Instant Scan", lang)
title80 = html.escape(html.unescape(str(meta.get('title') or _L('(없음)', '(none)', lang))))[:80]
pct, grade = _overall(d)
body = ('<div style="font-family:\'Malgun Gothic\',sans-serif;max-width:880px;margin:0 auto;color:#0e1621">'
'<div style="display:flex;gap:20px;align-items:center;background:linear-gradient(135deg,#2e1065,#4c1d95 60%,#7c3aed 150%);color:#fff;border-radius:18px;padding:24px 26px;margin-bottom:14px">'
'<div style="flex:1">'
'<div style="font-size:12px;letter-spacing:5px;color:#d6bcff;font-weight:800">C H I T O S · ' + kind + '</div>'
'<div style="font-size:24px;font-weight:800;margin:8px 0 2px;color:#fff">' + _L("보안 · SEO · AI가시성 진단 리포트", "Security · SEO · AI-Visibility Report", lang) + '</div>'
'<div style="color:#e9d5ff;font-size:13px">' + _L("대상", "Target", lang) + ' ' + html.escape(host) + ' · ' + today + ' · ' + _L("비침투(읽기전용)", "non-intrusive (read-only)", lang) + '</div></div>'
+ _ring(pct, grade, lang, dark=True) + '</div>'
+ vbanner
+ '<div style="border:1px solid #e9e4f7;border-radius:14px;padding:14px 18px;margin-bottom:10px;background:#faf8ff">'
+ _scorebar(bS, d['sec_grade'])
+ _scorebar(bSEO, d.get('seo_grade', ['-', 0]))
+ _scorebar(bAI, d['geo_grade'])
+ '<div style="font-size:11px;color:#9ca3af;margin-top:4px">' + _L("자물쇠", "Locks", lang) + ' ' + str(d.get('locks_on', 0)) + '/6 · ' + _L("종합은 보안 45% · AI가시성 30% · SEO 25% 가중", "Overall = Security 45% · AI Visibility 30% · SEO 25% weighted", lang) + '</div></div>'
+ '<div style="display:flex;gap:10px;text-align:center;margin:10px 0">'
+ '<div style="flex:1;border:1px solid #e4e9ef;border-radius:10px;padding:10px"><b style="color:#b42318;font-size:22px">%d</b><div style="font-size:11px;color:#6b7280">%s</div></div>' % (nhi, _L("높음", "High", lang))
+ '<div style="flex:1;border:1px solid #e4e9ef;border-radius:10px;padding:10px"><b style="color:#b54708;font-size:22px">%d</b><div style="font-size:11px;color:#6b7280">%s</div></div>' % (nmid, _L("보완", "Advisory", lang))
+ '<div style="flex:1;border:1px solid #e4e9ef;border-radius:10px;padding:10px"><b style="color:#8a6d00;font-size:22px">%d</b><div style="font-size:11px;color:#6b7280">%s</div></div>' % (nlow, _L("낮음", "Low", lang))
+ '</div>'
+ _h2("🛡 " + _L("보안 점검", "Security checks", lang)) + _locks(d.get('locks_on', 0), lang) + _cards(sec, _L("✓ 외부에서 보이는 보안 위생 문제 없음", "✓ No externally visible security-hygiene issues", lang), lang) + _checklist(d.get('checks', {}).get('sec', []), lang)
+ _h2("📈 " + _L("SEO 토대 (검색 노출)", "SEO foundation (search visibility)", lang)) + _metacards(meta, lang) + _cards(seo, _L("✓ SEO 기본 요소 양호", "✓ SEO fundamentals are healthy", lang), lang) + _checklist(d.get('checks', {}).get('seo', []), lang)
+ _h2("🤖 " + _L("AI 로봇 접근 (AI 가시성)", "AI crawler access (AI visibility)", lang))
+ '<div style="font-size:12.5px;color:#4b5563;margin:4px 0 8px">' + _L("생성형 AI가 당신을 인용하려면 로봇이 먼저 들어와야 합니다.", "For generative AI to cite you, its crawlers must get in first.", lang) + '</div>'
+ _traffic(d.get('matrix', {}), lang) + _cards(geo, _L("✓ 기계 판독 신호 양호", "✓ Machine-readability signals are healthy", lang), lang) + _checklist(d.get('checks', {}).get('geo', []), lang)
+ '<div style="font-size:12px;color:#6b7280;margin-top:6px">' + _L("제목", "Title", lang) + ': ' + title80 + ' · H1 ' + str(meta.get('h1', 0)) + ' · ' + _L("구조화", "Schema", lang) + ' ' + str(meta.get('jsonld', 0)) + ' · ' + _L("홈", "Home", lang) + ' ' + str(meta.get('size_kb', 0)) + 'KB · alt ' + str(meta.get('alt_cov', 0)) + '%</div>'
+ deep_html
+ (_upsell(lang) if show_upsell else "")
+ _h2("✓ " + _L("잘 되어 있는 점", "What's done well", lang)) + '<div>' + good + '</div>'
+ _h2("📌 " + _L("조치 우선순위", "Action priorities", lang)) + _priority(allf, lang)
+ _consult_cta(nhi, nmid, nlow, lang)
+ '<div style="color:#9ca3af;font-size:11px;margin-top:14px;border-top:1px solid #e4e9ef;padding-top:10px">' + _L(
"본 진단은 비침투·비파괴로 수행되었으며 대상 서비스에 영향을 주지 않았습니다. 결과는 측정 시점(" + today + ") 기준. 서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS).",
"This diagnosis was non-intrusive and non-destructive and did not affect the target. Results as of " + today + ". Service: QuantID · Engine: CHITOS.", lang) + '</div>'
+ '</div>')
return body
def _save_html(body, host, deep):
full = "<!doctype html><html lang='ko'><head><meta charset='utf-8'><title>CHITOS %s</title></head><body style='background:#eef1f5;margin:0;padding:20px'>%s</body></html>" % (html.escape(host), body)
fn = os.path.join(tempfile.gettempdir(), "CHITOS_%s_%s.html" % (host.replace(".", "_"), "deep" if deep else "free"))
with open(fn, "w", encoding="utf-8") as f:
f.write(full)
return fn
def _spin(lang):
return ("""<div style="text-align:center;padding:40px 0;font-family:sans-serif">
<div style="width:46px;height:46px;border:5px solid #ede9fe;border-top-color:#6d28d9;border-radius:50%;margin:0 auto;animation:chspin .8s linear infinite"></div>
<div style="margin-top:16px;color:#6d28d9;font-weight:700">""" + _L("치토스가 점검 중입니다...", "CHITOS is scanning...", lang) + """</div>
<div style="margin-top:4px;color:#9ca3af;font-size:12px">""" + _L("보안 · SEO · AI 가시성을 읽는 중 (약 10~25초)", "Reading security · SEO · AI visibility (about 10–25s)", lang) + """</div>
<style>@keyframes chspin{to{transform:rotate(360deg)}}</style></div>""")
def _spin1(lang):
return _spin(lang)
def _loading(lang):
return gr.update(value=_spin(lang)), gr.update(visible=False)
def free_scan(url, lang):
if not (url or "").strip():
return _note(_L("진단할 주소를 입력하세요. 예: example.com", "Enter a URL to scan. e.g. example.com", lang), "mid"), None
d = _api("/scan", {"url": url})
return build_report(d, deep=False, lang=lang, show_upsell=True), d
def deep_scan(url, lang, profile: gr.OAuthProfile | None = None, oauth_token: gr.OAuthToken | None = None):
if profile is None or oauth_token is None:
return _note(_L('먼저 위의 <b>"Sign in with Hugging Face"</b> 버튼으로 로그인하세요. (회사/조직 도메인 메일로 가입한 계정)',
'First sign in with the <b>"Sign in with Hugging Face"</b> button above. (an account registered on your company/org domain email)', lang), "mid"), gr.update(visible=False), None
if not (url or "").strip():
return _note(_L("진단할 주소를 입력하세요.", "Enter a URL to scan.", lang), "mid"), gr.update(visible=False), None
try:
email = (whoami(token=oauth_token.token).get("email") or "").lower()
except Exception:
email = ""
if not email:
return _note(_L("로그인 계정의 인증된 이메일을 읽지 못했습니다. HF 계정 이메일 인증 후 다시 시도하세요.",
"Could not read a verified email from your account. Verify your HF account email and try again.", lang), "hi"), gr.update(visible=False), None
edom = email.split("@")[-1]
tdom = _domain(url)
if edom != tdom and not tdom.endswith("." + edom) and not edom.endswith("." + tdom):
return _note(_L("도메인 소유 확인 실패. 진단 대상(<b>%s</b>)의 도메인 이메일로 가입한 HF 계정으로 로그인해야 합니다. 현재 계정 도메인: <b>%s</b>" % (html.escape(tdom), html.escape(edom)),
"Ownership check failed. Sign in with an HF account on the target domain (<b>%s</b>). Your account domain: <b>%s</b>" % (html.escape(tdom), html.escape(edom)), lang), "hi"), gr.update(visible=False), None
d = _api("/deep", {"url": url})
body = build_report(d, deep=True, verified_email=email, lang=lang)
if d.get("error") or d.get("blocked"):
return body, gr.update(visible=False), None
return body, gr.update(value=_save_html(body, d.get("host", "site"), True), visible=True), {"d": d, "email": email}
def consult(area, name, company, email, phone, msg, lang):
if not (name and email):
return _note(_L("이름과 이메일은 필수입니다.", "Name and email are required.", lang), "mid")
if not CONSULT:
return _note(_L("상담 접수처가 아직 설정되지 않았습니다.", "Consultation intake is not configured yet.", lang), "mid")
try:
rr = requests.post(CONSULT, json={"진단분야": area or "-", "이름": name, "회사": company or "-", "이메일": email, "연락처": phone or "-", "문의": msg or "-", "_subject": "[CHITOS] consult — %s" % (area or "")}, timeout=15)
if rr.status_code < 300:
return _note(_L("상담 신청이 접수됐습니다. 퀀트ID(QuantID)에서 곧 연락드립니다.", "Your request was received. QuantID will contact you shortly.", lang), "ok")
return _note(_L("접수 중 문제가 발생했습니다. 다시 시도해 주세요.", "Something went wrong submitting. Please try again.", lang), "mid")
except Exception:
return _note(_L("접수 연결 실패. 잠시 후 다시 시도해 주세요.", "Submission connection failed. Please try again shortly.", lang), "mid")
# ===== 언어별 설명/가이드 =====
def _steplead(step, title, lead, bullets, foot):
lis = "".join('<li style="margin:4px 0">%s</li>' % b for b in bullets)
return ('<div style="border:1px solid #e9e4f7;border-radius:12px;padding:14px 16px;margin:4px 0 12px;background:#faf8ff">'
'<div style="font-weight:800;color:#2e1065;font-size:15px">%s %s</div>'
'<div style="color:#4b5563;font-size:13px;margin:6px 0 8px">%s</div>'
'<ul style="margin:0;padding-left:18px;color:#374151;font-size:12.8px">%s</ul>'
'<div style="color:#8a6d00;font-size:11.5px;margin-top:8px">%s</div></div>') % (step, title, lead, lis, foot)
def desc1(lang):
if lang == "ko":
return _steplead("①", "무료 즉시 진단 (FREE)",
"주소만 넣으면 <b>약 10~25초</b> 안에 공개적으로 드러난 보안·검색·AI 노출 위생을 <b>비침투(읽기전용)</b>로 점검합니다. 공격·부하·로그인 없이 홈 1페이지를 읽어 핵심만 요약합니다.",
["<b>보안 위생</b> — HTTPS 강제·HSTS, 보안 응답 헤더(CSP·X-Frame-Options 등), 쿠키 속성, 서버 버전 노출, 민감·백업 파일 노출",
"<b>SEO 토대</b> — 제목·메타설명·canonical·구조화데이터·이미지 대체텍스트 등 검색 노출의 기본기",
"<b>AI 가시성(GEO)</b> — 생성형 AI·검색 로봇 <b>9종</b>(Googlebot·Bingbot·네이버 Yeti·GPTBot·ClaudeBot·PerplexityBot·OAI-SearchBot 등) 도달성",
"<b>등급 카드</b> — 보안·SEO·AI가시성 3축 점수 + 가장 급한 상위 이슈"],
"본 단계는 누구나·대상 제한 없이 가능하며 대상 서비스에 영향을 주지 않습니다(읽기전용).")
return _steplead("①", "Free Instant Scan",
"Enter a URL and within <b>~10–25s</b> CHITOS checks publicly visible security, search and AI-exposure hygiene <b>non-intrusively (read-only)</b> — no attack, no load, no login. It reads one home page and summarizes the essentials.",
["<b>Security hygiene</b> — forced HTTPS/HSTS, security response headers (CSP, X-Frame-Options, …), cookie flags, server-version exposure, sensitive/backup file exposure",
"<b>SEO foundation</b> — title, meta description, canonical, structured data, image alt-text and other search basics",
"<b>AI visibility (GEO)</b> — reachability of <b>9</b> generative-AI/search crawlers (Googlebot, Bingbot, Naver Yeti, GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot, …)",
"<b>Grade cards</b> — Security / SEO / AI-Visibility scores + the most urgent issues"],
"This step is open to anyone, for any target, and does not affect the target service (read-only).")
def desc2(lang):
if lang == "ko":
return _steplead("②", "도메인 인증 심화 진단",
"<b>회사/조직 도메인 이메일로 가입한 Hugging Face 계정</b>으로 로그인하면, 이메일 도메인과 진단 대상 도메인이 일치하는지로 <b>소유를 확인</b>합니다(별도 인증코드 없음). 소유가 확인되면 전사이트 다중 페이지를 크롤링한 정밀 리포트가 열립니다. 여전히 <b>비침투</b>입니다.",
["<b>전사이트 크롤</b> — 사이트맵 기반 다중 페이지 점검(페이지별 이슈·색인차단·canonical·얇은 페이지)",
"<b>구조화데이터 커버리지</b> — Organization·Product·Service·FAQ 등 Schema 보유/누락",
"<b>이메일 위조방지</b> — SPF·DMARC 레코드 점검(사칭·피싱 방어)",
"<b>자기모순 탐지</b> — 주소·전화·사업자번호가 페이지마다 다른지(AI 신뢰도 핵심)",
"<b>조치 우선순위 + 다운로드 리포트(HTML)</b>"],
"소유 인증은 로그인 계정의 인증된 이메일 도메인으로만 판정합니다. 타인 사이트 무단 정밀점검은 불가합니다.")
return _steplead("②", "Domain-Verified Deep Scan",
"Sign in with a <b>Hugging Face account registered on your company/org domain email</b>. Ownership is confirmed by matching your email domain to the target domain (no separate verification code). Once confirmed, a full-site, multi-page detailed report opens. Still <b>non-intrusive</b>.",
["<b>Full-site crawl</b> — sitemap-based multi-page checks (per-page issues, noindex, canonical, thin pages)",
"<b>Structured-data coverage</b> — Organization/Product/Service/FAQ schema present or missing",
"<b>Email anti-spoofing</b> — SPF/DMARC record checks (impersonation/phishing defense)",
"<b>Self-contradiction detection</b> — address/phone/business-number differing across pages (key for AI trust)",
"<b>Action priorities + downloadable report (HTML)</b>"],
"Ownership is judged only by your account's verified email domain. Unauthorized deep scans of others' sites are not allowed.")
def desc3(lang):
if lang == "ko":
return _steplead("③", "정밀 진단 상담",
"비침투 스캔 너머의 <b>실제 취약점</b>과 AI 시대 심층 진단은 전문가가 <b>서면 허가 기반</b>으로 수행합니다. 아래에서 <b>진단 분야</b>를 선택하고 신청하세요.",
["SQL 삽입·파일 업로드·인증/세션 우회 등 <b>실제 웹 취약점</b> 능동 점검(허가 후)",
"소스 코드 취약점 점검(시큐어코딩)",
"금융보안원 「금융분야 AI 에이전트 보안 평가기준」(6개 분야 17항목) 대응 레드티밍"],
"능동 점검은 반드시 대상 소유자의 서면 허가 범위 안에서만 수행합니다.")
return _steplead("③", "In-Depth Consultation",
"Beyond non-intrusive scanning, <b>real vulnerabilities</b> and AI-era deep diagnosis are performed by experts <b>under written authorization</b>. Pick a <b>diagnosis area</b> below and request a consult.",
["Active testing of <b>real web vulnerabilities</b> — SQLi, file upload, auth/session bypass (after authorization)",
"Source-code vulnerability review (secure coding)",
"Red-teaming against the FSI 「Financial-Sector AI-Agent Security Evaluation Criteria」 (6 domains, 17 items)"],
"Active testing is performed strictly within the target owner's written authorization scope.")
AREAS = [
"웹서비스 보안진단 · Web Service",
"AI 모델 안전진단 · AI Model Safety",
"AX 인프라 보안진단 · AX Infra",
"AI 에이전트 보안진단 · AI Agent (AX-RAY)",
]
def area_help(lang):
if lang == "ko":
cards = [("🌐 웹서비스 보안진단", "웹 애플리케이션 실제 취약점(주입·업로드·인증/세션)·소스 시큐어코딩·능동 점검(허가 기반)."),
("🧠 AI 모델 안전진단", "탈옥·유해·편향·민감정보 유출 등 모델 자체의 안전성 평가."),
("🏗 AX 인프라 보안진단", "AI 전환(AX) 파이프라인·배포·접근통제·비밀/키 관리·로그 등 운영 인프라."),
("🤖 AI 에이전트 보안진단", "에이전트 레드티밍 — 프롬프트 주입·도구 오남용·권한상승·과도 대리행위. (AX-RAY 역량 기반)")]
head = "진단 분야 선택"
else:
cards = [("🌐 Web Service", "Real web-app vulnerabilities (injection, upload, auth/session), secure-coding review, active testing (authorized)."),
("🧠 AI Model Safety", "Model-level safety: jailbreak, harmful/biased output, sensitive-info leakage."),
("🏗 AX Infra Security", "AI-transformation infra: pipelines, deployment, access control, secret/key management, logging."),
("🤖 AI Agent Security", "Agent red-teaming — prompt injection, tool misuse, privilege escalation, excessive agency. (AX-RAY capability)")]
head = "Choose a diagnosis area"
grid = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:2px 0 10px">'
for t, dsc in cards:
grid += ('<div style="border:1px solid #e4e9ef;border-radius:9px;padding:9px 12px"><b style="color:#2e1065">%s</b>'
'<div style="font-size:12px;color:#4b5563;margin-top:3px">%s</div></div>') % (t, dsc)
grid += '</div>'
return '<div style="font-weight:700;color:#2e1065;font-size:13px;margin:6px 0 4px">' + head + '</div>' + grid
def _artex_section(lang):
if lang == "ko":
head = "🚨 왜 지금인가 — 에이전트형(agentic) 공격의 부상"
bullets = ["<b>AI 침투 에이전트</b>가 정찰→침투→탈취를 사람 개입 없이 <b>자동</b>으로 수행하는 공격이 현실화",
"2026년 오픈소스 에이전트 <b>‘ARTEX’</b>가 실제 공격에 동원 — <b>Microsoft 365 2,250여 테넌트</b> 공격, 전자상거래 주문 DB 침해",
"공통 진입점은 <b>인터넷에 노출된 미패치 시스템</b>과 <b>약한 웹·인프라 위생</b> — 전 세계·전 산업 공통",
"공격 표면 = <b>웹·인프라 위생</b> + <b>AI 에이전트 권한·도구 오남용</b> (이중용도 도구의 악용)"]
resp = "대응: <b>웹서비스 · AX 인프라 · AI 모델 · AI 에이전트(AX-RAY)</b> 4대 진단으로 공격·방어 양면을 점검합니다."
src = "참고(2026): therecord.media · bankinfosecurity · techtimes.com"
else:
head = "🚨 Why now — the rise of agentic attacks"
bullets = ["<b>AI penetration agents</b> now run recon → breach → exfiltration <b>autonomously</b>, with no human in the loop",
"In 2026 the open-source agent <b>“ARTEX”</b> was used in real attacks — hitting <b>2,250+ Microsoft 365 tenants</b> and breaching e-commerce order databases",
"The common entry point is <b>unpatched internet-facing systems</b> and <b>weak web/infra hygiene</b> — across industries worldwide",
"Attack surface = <b>web/infra hygiene</b> + <b>AI-agent privilege & tool misuse</b> (abuse of a dual-use tool)"]
resp = "Response: four diagnoses — <b>Web Service · AX Infra · AI Model · AI Agent (AX-RAY)</b> — covering both attack and defense."
src = "References (2026): therecord.media · bankinfosecurity · techtimes.com"
lis = "".join('<li style="margin:3px 0">%s</li>' % b for b in bullets)
return ('<div style="border:1px solid #fecaca;background:#fef2f2;border-radius:12px;padding:13px 16px;margin-bottom:14px">'
'<b style="color:#b42318;font-size:14px">' + head + '</b>'
'<ul style="margin:8px 0 6px;padding-left:18px;font-size:12.5px;color:#374151;line-height:1.6">' + lis + '</ul>'
'<div style="font-size:12.5px;color:#2e1065;background:#f5f3ff;border-radius:8px;padding:8px 11px;margin-top:4px">🛡 ' + resp + '</div>'
'<div style="font-size:10.5px;color:#9ca3af;margin-top:6px">' + src + '</div></div>')
def guideline_html(lang):
ko = (lang == "ko")
def chk(items):
return "".join('<div style="font-size:12.5px;color:#374151;padding:3px 0">☐ %s</div>' % x for x in items)
row = lambda a, dom, intl: ('<tr><td style="border:1px solid #e4e9ef;padding:8px;font-weight:700;color:#2e1065;white-space:nowrap">%s</td>'
'<td style="border:1px solid #e4e9ef;padding:8px;font-size:12.5px">%s</td>'
'<td style="border:1px solid #e4e9ef;padding:8px;font-size:12.5px">%s</td></tr>') % (a, dom, intl)
if ko:
hero_t = "국내외 보안·AI 안전 기준 수용 현황 · 자가 점검 체크리스트"
hero_s = "치토스 진단은 아래 공개 기준을 <b>참조·반영</b>합니다. (내부 점검 로직·공격셋·임계값은 비공개)"
h_map = "🧭 진단 분야 × 대표 준거 매핑"
th = ("진단 분야", "국내 기준", "국제 기준")
rows = [row("웹서비스<br>보안진단", "KISA 웹 취약점 진단·제거 가이드 · KISA 소프트웨어 보안약점(시큐어코딩) 가이드 · ISMS-P", "OWASP Top 10 · OWASP ASVS · NIST SSDF(SP 800-218)"),
row("AI 모델<br>안전진단", "과기정통부·TTA AI 신뢰성/안전 지침 · 금융보안원 AI 평가기준(일부)", "OWASP Top 10 for LLM Apps · NIST AI RMF · MITRE ATLAS · EU AI Act · ISO/IEC 42001"),
row("AX 인프라<br>보안진단", "ISMS-P · 개인정보 안전성 확보조치 기준 · 전자금융감독규정", "ISO/IEC 27001 · NIST SSDF · OWASP ASVS · Google SAIF"),
row("AI 에이전트<br>보안진단", "금융보안원 「금융분야 AI 에이전트 보안 평가기준」(6개 분야 17항목, 2026.10) · 전자금융감독규정", "OWASP LLM Top 10(과도 대리·도구 오남용) · MITRE ATLAS · NIST AI RMF")]
note = "※ 금융보안원 기준은 2027년 금융권 레드티밍 정식 평가기준입니다."
h_all = "📚 수용 기준 전체 목록"
dom_t = "🇰🇷 국내"; intl_t = "🌍 국제"
dom_list = ["금융보안원 「금융분야 AI 에이전트 보안 평가기준」", "전자금융감독규정 · 전자금융거래법", "KISA 홈페이지(웹) 취약점 진단·제거 가이드",
"KISA 소프트웨어 보안약점 진단가이드(시큐어코딩)", "ISMS-P(정보보호·개인정보보호 관리체계)", "개인정보 안전성 확보조치 기준(개인정보보호위)", "과기정통부·TTA AI 신뢰성/안전 지침"]
intl_list = ["OWASP Top 10 (웹)", "OWASP Top 10 for LLM Applications", "OWASP ASVS(앱 보안 검증 표준)", "MITRE ATT&CK · MITRE ATLAS(AI 적대적 위협)",
"NIST AI RMF(AI 위험관리) · NIST SSDF", "ISO/IEC 27001 · ISO/IEC 42001(AI 경영시스템)", "EU AI Act · Google SAIF"]
h_chk = "✅ 자가 점검 체크리스트 (공개 베스트프랙티스)"
c1_t = "웹 · 인프라"; c2_t = "AI 모델 · 에이전트"; c3_t = "이메일 · 도메인"; c4_t = "개인정보"
c1 = ["HTTPS 전면 적용 + HSTS", "보안 응답 헤더(CSP·X-Frame-Options·X-Content-Type-Options·Referrer-Policy·Permissions-Policy)", "쿠키 Secure·HttpOnly·SameSite",
"민감·백업 파일(.env·.git·*.bak) 공개 차단", "서버·프레임워크 버전 숨김 + 최신 패치", "관리자 페이지 접근통제 + 다중인증(MFA)", "비밀·키 하드코딩 금지 + 비밀관리 체계"]
c2 = ["프롬프트 주입·탈옥 방어(입력 검증·시스템 프롬프트 격리)", "도구·함수 호출 권한 최소화 + 위험행위 승인 게이트", "민감정보(PII·비밀) 출력 차단 필터",
"과도한 대리행위(자동 송금·삭제 등) 킬스위치", "감사 로그(에이전트 결정·도구호출 기록)", "유해·편향 출력 모니터링", "모델·데이터 공급망(SBOM) 관리"]
c3 = ["SPF 설정", "DKIM 서명", "DMARC 정책 적용", "발신 도메인 사칭 모니터링"]
c4 = ["수집·보관 최소화", "저장·전송 암호화", "접근 권한·접근기록 관리", "파기 절차"]
foot = "본 체크리스트는 공개 베스트프랙티스 요약입니다. 치토스 정밀 진단은 각 항목을 <b>실제 환경에서 측정·검증</b>하고 항목별 처방을 제공합니다. 점검 방법·공격셋·판정 기준 등 내부 방법론은 비공개입니다. 능동 점검은 <b>서면 허가 범위</b> 안에서만 수행합니다."
svc = "서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)"
else:
hero_t = "Domestic & international security / AI-safety standards we incorporate · self-check checklist"
hero_s = "CHITOS diagnosis <b>references and reflects</b> the public standards below. (internal logic, attack sets and thresholds are confidential)"
h_map = "🧭 Diagnosis area × key standards mapping"
th = ("Diagnosis area", "Domestic (KR)", "International")
rows = [row("Web Service", "KISA web vuln. guide · KISA secure-coding guide · ISMS-P", "OWASP Top 10 · OWASP ASVS · NIST SSDF (SP 800-218)"),
row("AI Model<br>Safety", "MSIT/TTA AI trustworthiness guidance · FSI AI criteria (partial)", "OWASP Top 10 for LLM Apps · NIST AI RMF · MITRE ATLAS · EU AI Act · ISO/IEC 42001"),
row("AX Infra<br>Security", "ISMS-P · Personal-data safeguards · E-Financial Supervisory Rules", "ISO/IEC 27001 · NIST SSDF · OWASP ASVS · Google SAIF"),
row("AI Agent<br>Security", "FSI 「Financial AI-Agent Security Criteria」 (6 domains, 17 items, 2026.10) · E-Financial Supervisory Rules", "OWASP LLM Top 10 (excessive agency, tool misuse) · MITRE ATLAS · NIST AI RMF")]
note = "※ The FSI criteria are the official 2027 financial-sector red-teaming standard."
h_all = "📚 Full list of incorporated standards"
dom_t = "🇰🇷 Domestic (KR)"; intl_t = "🌍 International"
dom_list = ["FSI 「Financial-Sector AI-Agent Security Evaluation Criteria」", "E-Financial Supervisory Rules · E-Financial Transactions Act", "KISA website (web) vulnerability guide",
"KISA software security-weakness (secure coding) guide", "ISMS-P (info & privacy management system)", "Personal-data safeguards standard (PIPC)", "MSIT/TTA AI trustworthiness/safety guidance"]
intl_list = ["OWASP Top 10 (web)", "OWASP Top 10 for LLM Applications", "OWASP ASVS", "MITRE ATT&CK · MITRE ATLAS (adversarial AI threats)",
"NIST AI RMF · NIST SSDF", "ISO/IEC 27001 · ISO/IEC 42001 (AI management system)", "EU AI Act · Google SAIF"]
h_chk = "✅ Self-check checklist (public best practices)"
c1_t = "Web · Infra"; c2_t = "AI Model · Agent"; c3_t = "Email · Domain"; c4_t = "Personal data"
c1 = ["HTTPS everywhere + HSTS", "Security response headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)", "Cookie Secure/HttpOnly/SameSite",
"Block sensitive/backup files (.env, .git, *.bak)", "Hide server/framework version + latest patches", "Admin-page access control + MFA", "No hardcoded secrets/keys + secret management"]
c2 = ["Prompt-injection/jailbreak defense (input validation, system-prompt isolation)", "Least-privilege tool/function calls + approval gate for risky actions", "Sensitive-info (PII/secret) output filter",
"Kill-switch for excessive agency (auto transfer/delete, …)", "Audit logs (agent decisions, tool calls)", "Harmful/biased output monitoring", "Model/data supply chain (SBOM) management"]
c3 = ["SPF configured", "DKIM signing", "DMARC policy applied", "Sender-domain spoofing monitoring"]
c4 = ["Minimize collection/retention", "Encrypt at rest & in transit", "Access rights & access logging", "Disposal procedure"]
foot = "This checklist is a summary of public best practices. CHITOS in-depth diagnosis <b>measures and verifies each item in the real environment</b> and gives per-item remediation. Internal methodology (methods, attack sets, decision thresholds) is confidential. Active testing is performed only within <b>written-authorization scope</b>."
svc = "Service: QuantID · Engine: CHITOS"
html_out = (
'<div style="font-family:sans-serif;max-width:900px;margin:0 auto;color:#0e1621">'
'<div class="chdk" style="background:linear-gradient(135deg,#2e1065,#6d28d9);color:#fff;border-radius:14px;padding:18px 20px;margin-bottom:12px">'
'<div style="font-size:12px;letter-spacing:4px;color:#d6bcff;font-weight:800">C H I T O S · ' + _L("준거 체계", "Standards", lang) + '</div>'
'<div style="font-size:20px;font-weight:800;margin-top:6px;color:#fff">' + hero_t + '</div>'
'<div style="color:#e9d5ff;font-size:12.5px;margin-top:4px">' + hero_s + '</div></div>'
+ _artex_section(lang)
+ _h2(h_map)
+ '<table style="width:100%;border-collapse:collapse"><tr>'
'<th style="border:1px solid #e4e9ef;padding:8px;background:#f5f3ff;text-align:left">' + th[0] + '</th>'
'<th style="border:1px solid #e4e9ef;padding:8px;background:#f5f3ff;text-align:left">' + th[1] + '</th>'
'<th style="border:1px solid #e4e9ef;padding:8px;background:#f5f3ff;text-align:left">' + th[2] + '</th></tr>'
+ "".join(rows) + '</table>'
+ '<div style="font-size:11.5px;color:#6b7280;margin-top:6px">' + note + '</div>'
+ _h2(h_all)
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
'<div style="border:1px solid #e4e9ef;border-radius:10px;padding:12px 14px"><b style="color:#2e1065">' + dom_t + '</b>'
'<ul style="margin:6px 0 0;padding-left:18px;font-size:12.5px;color:#374151;line-height:1.7">' + "".join("<li>%s</li>" % x for x in dom_list) + '</ul></div>'
'<div style="border:1px solid #e4e9ef;border-radius:10px;padding:12px 14px"><b style="color:#2e1065">' + intl_t + '</b>'
'<ul style="margin:6px 0 0;padding-left:18px;font-size:12.5px;color:#374151;line-height:1.7">' + "".join("<li>%s</li>" % x for x in intl_list) + '</ul></div></div>'
+ _h2(h_chk)
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
+ '<div style="border:1px solid #e4e9ef;border-radius:10px;padding:12px 14px"><b style="color:#b54708">' + c1_t + '</b>' + chk(c1) + '</div>'
+ '<div style="border:1px solid #e4e9ef;border-radius:10px;padding:12px 14px"><b style="color:#b54708">' + c2_t + '</b>' + chk(c2) + '</div>'
+ '<div style="border:1px solid #e4e9ef;border-radius:10px;padding:12px 14px"><b style="color:#b54708">' + c3_t + '</b>' + chk(c3) + '</div>'
+ '<div style="border:1px solid #e4e9ef;border-radius:10px;padding:12px 14px"><b style="color:#b54708">' + c4_t + '</b>' + chk(c4) + '</div>'
+ '</div>'
+ '<div style="background:#f4f0ff;border:1px dashed #b89cff;border-radius:12px;padding:13px 16px;margin-top:14px;font-size:12.5px;color:#374151">' + foot + '</div>'
+ '<div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">' + svc + '</div>'
+ '</div>')
return html_out
def threat_banner(lang):
if lang == "ko":
t = ("⚠ <b>AI 에이전트가 스스로 침투하는 시대입니다.</b> 2026년 오픈소스 AI 침투 에이전트 ‘ARTEX’가 실제 표적에 동원되어 "
"<b>Microsoft 365 2,250여 테넌트</b> 공격, 전자상거래 주문 DB 침해 등 <b>인터넷에 노출된 미패치 시스템</b>을 정찰부터 탈취까지 <b>자동</b>으로 수행했습니다. "
"공격이 자동화된 지금, 방어도 자동화되어야 합니다 — <b>웹·인프라·AI 모델·AI 에이전트</b>를 함께 진단하세요.")
else:
t = ("⚠ <b>AI agents now break in on their own.</b> In 2026 the open-source AI penetration agent “ARTEX” was turned on real targets — "
"hitting <b>2,250+ Microsoft 365 tenants</b>, breaching e-commerce order databases, and <b>automating recon-to-exfiltration</b> against "
"<b>unpatched internet-facing systems</b> worldwide. Attacks are automated now — so must defense be. Diagnose your <b>web, infra, AI models and AI agents</b> together.")
return ('<div class="chdk" style="background:#1b0f2e;border:1px solid #7c3aed;border-radius:12px;padding:12px 16px;margin:4px 0 2px;'
'color:#f3e8ff;font-size:12.5px;line-height:1.55">' + t + '</div>')
def header_html(lang):
return ('<div id="title" style="padding:10px 0 4px"><div style="font-size:13px;letter-spacing:5px;color:#8b5cf6;font-weight:800">C H I T O S</div>'
'<h1 style="margin:6px 0;color:#2e1065">' + _L("보안 · SEO · AI가시성 진단", "Security · SEO · AI-Visibility Diagnosis", lang) + '</h1>'
'<div style="color:#6b7280">' + _L("① 무료 즉시 리포트 → ② 도메인 메일 HF 로그인 심화 리포트 → ③ 정밀 진단 상담 · ④ 준거·체크리스트",
"① Free instant report → ② Domain-email HF login deep report → ③ In-depth consult · ④ Standards & checklist", lang) + '</div></div>')
def footer_html(lang):
return '<div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">' + _L(
"비침투·비파괴 · 타인 사이트 무단 정밀점검 금지 · 능동 점검은 도메인 인증·허가 후 · 서비스 퀀트ID(QuantID) · 엔진 치토스(CHITOS)",
"Non-intrusive & non-destructive · no unauthorized deep scans of others' sites · active testing only after domain verification/authorization · Service: QuantID · Engine: CHITOS", lang) + '</div>'
def relang(label, last_scan, last_deep):
lang = "en" if str(label).startswith("Eng") else "ko"
o1u = gr.update(value=build_report(last_scan, deep=False, lang=lang, show_upsell=True)) if last_scan else gr.update()
o2u = gr.update(value=build_report(last_deep["d"], deep=True, verified_email=last_deep.get("email"), lang=lang)) if last_deep else gr.update()
return (lang, gr.update(value=label), header_html(lang), threat_banner(lang), desc1(lang), desc2(lang), desc3(lang),
area_help(lang), guideline_html(lang), footer_html(lang), o1u, o2u)
CSS = "#title{text-align:center}.gradio-container{max-width:980px!important;margin:0 auto}.chdk b,.chdk strong{color:#fff}"
with gr.Blocks(title="CHITOS 인스턴트 스캔", css=CSS, theme=gr.themes.Soft(primary_hue="purple")) as demo:
lang_state = gr.State("ko")
last_scan = gr.State(None)
last_deep = gr.State(None)
hdr = gr.HTML(header_html("ko"))
g_threat = gr.HTML(threat_banner("ko"))
with gr.Row():
lang_radio = gr.Radio(["한국어", "English"], value="한국어", label="🌐 언어 / Language", min_width=260)
with gr.Tabs():
with gr.Tab("① 무료 · FREE"):
g_d1 = gr.HTML(desc1("ko"))
with gr.Row():
u1 = gr.Textbox(label="", placeholder="example.com", scale=4, container=False)
b1 = gr.Button("진단 / Scan", variant="primary", scale=1)
o1 = gr.HTML()
b1.click(_spin1, lang_state, o1).then(free_scan, [u1, lang_state], [o1, last_scan])
u1.submit(_spin1, lang_state, o1).then(free_scan, [u1, lang_state], [o1, last_scan])
with gr.Tab("② 심화 · DEEP"):
g_d2 = gr.HTML(desc2("ko"))
gr.LoginButton()
u2 = gr.Textbox(label="진단할 주소 / Target URL", placeholder="example.com")
b2 = gr.Button("인증 후 심화 진단 / Verify & deep scan", variant="primary")
f2 = gr.File(label="리포트 다운로드 / Download report (HTML)", visible=False)
o2 = gr.HTML()
b2.click(_loading, lang_state, [o2, f2]).then(deep_scan, inputs=[u2, lang_state], outputs=[o2, f2, last_deep])
with gr.Tab("③ 상담 · CONSULT"):
g_d3 = gr.HTML(desc3("ko"))
g_ah = gr.HTML(area_help("ko"))
ar = gr.Radio(AREAS, value=AREAS[0], label="진단 분야 / Diagnosis area")
with gr.Row():
n = gr.Textbox(label="이름 Name*"); co = gr.Textbox(label="회사 Company")
with gr.Row():
em = gr.Textbox(label="이메일 Email*"); ph = gr.Textbox(label="연락처 Phone")
mg = gr.Textbox(label="문의 내용 Message", lines=3, placeholder="대상 시스템·범위·일정·허가 여부 / target, scope, timeline, authorization")
bc = gr.Button("상담 신청 / Request consult", variant="primary"); oc = gr.HTML()
bc.click(consult, [ar, n, co, em, ph, mg, lang_state], oc)
with gr.Tab("④ 준거 · STANDARDS"):
g_gl = gr.HTML(guideline_html("ko"))
g_ft = gr.HTML(footer_html("ko"))
_ins = [lang_radio, last_scan, last_deep]
_outs = [lang_state, lang_radio, hdr, g_threat, g_d1, g_d2, g_d3, g_ah, g_gl, g_ft, o1, o2]
lang_radio.change(relang, _ins, _outs)
demo.load(relang, _ins, _outs,
js="() => [ (navigator.language||'en').toLowerCase().startsWith('ko') ? '한국어' : 'English', null, null ]")
if __name__ == "__main__":
demo.launch()