# -*- 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 ('
' '
' '
%s
' '
%s/100
' '
%s
') % ( 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 ('
' '
%s
' '
' '
' '
%s·%s
') % ( label, s, col, col, col, L, s) def _locks(n, lang): cells = "" for i in range(6): on = i < n cells += ('
' '
%s
') % ("#ecfdf3" if on else "#fef3f2", "🔒" if on else "🔓") cap = _L("보안 응답 헤더 6종 중 %d개 적용", "%d of 6 security response headers applied", lang) % n return ('
🔐 ' + cap + '
' '
' + cells + '
') 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('
' '
%s
' '
%s
' % (v, k) for k, v in data) return '
' + cells + '
' def _consult_cta(nhi, nmid, nlow, lang): total = nhi + nmid + nlow if lang == "ko": risk = '%d개 개선점(높음 %d · 보완 %d · 낮음 %d)' % (total, nhi, nmid, nlow) body = ("이 진단에서 " + risk + "이 확인됐습니다. 하지만 비침투 스캔은 ‘밖에서 보이는 표면’까지입니다 — " "SQL삽입·파일업로드·인증우회·IDOR 같은 실제 취약점, 그리고 ARTEX처럼 AI 에이전트가 자동으로 뚫는 위협은 " "능동 정밀 진단으로만 확인됩니다.") areas = "웹서비스 · AX 인프라 · AI 모델 · AI 에이전트(AX-RAY) 4대 진단 + 금융보안원 17항목 레드티밍" go = "👉 ③ 정밀 진단 상담 탭에서 분야를 선택해 신청하세요. (서면 허가 기반·국내외 준거 수용)" head = "🎯 다음 단계 — 정밀 진단 상담" else: risk = '%d improvement points (High %d · Advisory %d · Low %d)' % (total, nhi, nmid, nlow) body = ("This scan found " + risk + ". But a non-intrusive scan only shows the surface — " "real vulnerabilities like SQLi, file upload, auth bypass and IDOR, and threats where an AI agent breaks in automatically (as with ARTEX), " "are confirmed only by active in-depth testing.") areas = "Web Service · AX Infra · AI Model · AI Agent (AX-RAY) — 4 diagnoses + FSI 17-item red-teaming" go = "👉 Request it in the ③ In-Depth Consultation tab (authorization-based · aligned to domestic & international standards)." head = "🎯 Next step — in-depth consultation" return ('
' '
' + head + '
' '
' + body + '
' '
🛡 ' + areas + '
' '
' + go + '
') def _upsell(lang): return ('
' '
🔓 ' + _L("전사이트 정밀 리포트 — ② 도메인 인증 탭", "Full-site report — ② Domain Verification tab", lang) + '
' '
' + _L( "이 리포트는 홈 1페이지 기준입니다. 회사/조직 도메인 메일로 가입한 Hugging Face 계정으로 로그인하면 — 전사이트 페이지별 이슈 · 구조화데이터 커버리지 · 이메일 위조방지(SPF/DMARC) · 자기모순(주소·전화) 탐지 · 다운로드 리포트까지 받습니다.", "This report is based on the home page. Sign in with a Hugging Face account on your company/org domain email to unlock — per-page issues across the whole site · structured-data coverage · email anti-spoofing (SPF/DMARC) · self-contradiction (address/phone) detection · downloadable report.", lang) + '
') 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 = ('%s' % html.escape(str(val))) if val else "" return ('
' '%s' '%s%s
') % (col, sym, html.escape(label), v) def _email_block(ed, es, lang): if not ed: return ('
' + _statrow("SPF", bool(es.get("spf")), warn=not es.get("spf")) + _statrow("DMARC", bool(es.get("dmarc")), warn=not es.get("dmarc")) + '
') 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 '
' + "".join(rows) + '
' 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 '
' + "".join(rows) + '
' 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('%s' % html.escape(x) for x in ext) extd = '
' + _L("외부 스크립트 출처: ", "Third-party script sources: ", lang) + '
' + chips return '
' + "".join(rows) + '
' + 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 += ('
' '%s' '%s
') % (bg, col, sym, html.escape(_P(lab, lang))) head = (_L("점검 항목 전체", "All checks", lang) + ' · %d/%d %s' % (npass, len(items), _L("통과", "pass", lang))) return ('
📋 ' + head + '
' '
' + cells + '
') def _cards(items, empty, lang): if not items: return '
%s
' % 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 += ('
' '[%s] %s
%s
') % ( 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 += ('
' '' '%s%s
') % (dot, html.escape(str(name)), txt) return '
%s
' % 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 '
%s
' % (c, bg, msg) def _h2(t): return '

%s

' % t def _priority(allf, lang): def li(lst, when, col): if not lst: return "" items = "".join("
  • %s
  • " % html.escape(_P(f[1], lang)) for f in lst) return ('
    ' '%s
    ') % (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 ("
    %s
    " % _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 ('
    ' '

    🛡 %s

    %s

    ') % ( _L("자동 공격 차단(WAF) 작동 중", "Automated attack blocking (WAF) active", lang), _L("비정상 접근을 탐지해 차단했습니다. 방어가 잘 되어 있다는 신호입니다. 치토스는 차단을 우회하지 않습니다.", "Abnormal access was detected and blocked. A sign your defenses work. 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 ('
    ' '
    ' + _ring(pct, grade, lang, dark=True) + '
    ' '
    C H I T O S · ' + _L("무료 즉시 진단", "Free Instant Scan", lang) + '
    ' '
    ' + html.escape(host) + '
    ' '
    ' + today + ' · ' + _L("홈 1페이지 · 비침투(읽기전용)", "home page · non-intrusive (read-only)", lang) + '
    ' '
    ' '🚨 ' + _L("높음", "High", lang) + ' ' + str(nhi) + '' '⚠ ' + _L("보완", "Advisory", lang) + ' ' + str(nmid) + '' '🤖 ' + _L("AI로봇", "AI bots", lang) + ' ' + str(ai_ok) + '/' + str(ai_tot) + '' '
    ' + '
    ' + _scorebar(bS, d["sec_grade"]) + _scorebar(bSEO, d.get("seo_grade", ["-", 0])) + _scorebar(bAI, d["geo_grade"]) + '
    ' + _L("종합은 보안 45% · AI가시성 30% · SEO 25% 가중 평균", "Overall = Security 45% · AI Visibility 30% · SEO 25% weighted", lang) + '
    ' + '

    ⚡ ' + _L("가장 급한 것 (상위 ", "Top priorities (top ", lang) + str(len(top)) + ')

    ' + topcards + '

    🤖 ' + _L("AI 로봇이 내 사이트에 들어올 수 있나", "Can AI crawlers reach your site?", lang) + '

    ' + _traffic(mx, lang) + '
    ' '
    🔓 ' + _L("전사이트 정밀 리포트가 더 있습니다", "A full-site detailed report is available", lang) + '
    ' '
    ' + _L( "지금은 홈 1페이지 요약입니다. ② 도메인 인증 탭에서 회사/조직 도메인 메일로 가입한 Hugging Face 계정으로 로그인하면 — 전사이트 페이지별 이슈 · 구조화데이터 커버리지 · 이메일 위조방지(SPF/DMARC) · 자기모순(주소·전화) 탐지 · 조치 우선순위 · 다운로드 리포트까지 받습니다.", "This is a home-page summary. In the ② Domain Verification tab, 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 · email anti-spoofing (SPF/DMARC) · self-contradiction (address/phone) detection · action priorities · downloadable report.", lang) + '
    ' '
    ' + _L("서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)", "Service: QuantID · Engine: CHITOS", lang) + '
    ') 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 ('
    ' '

    🛡 %s

    %s

    ') % ( _L("자동 공격 차단(WAF) 작동 중", "Automated attack blocking (WAF) active", lang), _L("비정상 접근을 탐지해 차단했습니다. 방어가 잘 되어 있다는 좋은 신호입니다. 치토스는 차단을 우회하지 않습니다(읽기전용).", "Abnormal access was detected and blocked. A good sign your defenses work. 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('✓ %s' % 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 '%s %s' % (c, c, ("✓" if ok else "✕"), t) sch = "".join('%s' % html.escape(x) for x in dp.get("schema_present", [])) or _L("없음", "none", lang) schm = "".join('%s' % 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 += '%s%s' % (_L("전화", "Phone", lang), html.escape(", ".join(dp["phones"]))) if len(dp.get("addrs", [])) > 1: r += '%s%s' % (_L("주소", "Address", lang), html.escape(" / ".join(dp["addrs"]))) if len(dp.get("bizno", [])) > 1: r += '%s%s' % (_L("사업자번호", "Biz reg. no.", lang), html.escape(", ".join(dp["bizno"]))) incon = ('

    ⚠ %s

    ' '%s
    %s%s
    ') % ( _L("확인 필요 — 사이트 안에서 다르게 보이는 값", "Please verify — values that differ across the site", lang), _L("항목", "Field", lang), _L("발견 값", "Found values", lang), r) pglist = "".join('
    %s %s %s
    ' % (('✓' 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 = ( '
    🔓 ' + (_L("소유 인증 전용 심화 — 전사이트 %d페이지 정밀 분석 (비침투)", "Verified-owner deep scan — %d pages analyzed (non-intrusive)", lang) % dp.get("pages", 0)) + '
    ' + _h2("🔎 " + _L("심화 발견", "Deep findings", lang)) + _cards(dp.get("issues", []), _L("✓ 심화 점검 추가 이슈 없음", "✓ No additional deep-scan issues", lang), lang) + _h2("📊 " + _L("전사이트 요약", "Full-site summary", lang)) + ('' '' '' '
    %s%d%s%d
    %s%d%s%d
    %s%d%s%d
    ') % ( _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)) + '
    ' + _L("보유:", "Present:", lang) + ' ' + sch + '
    ' + _L("누락:", "Missing:", lang) + ' ' + schm + '
    ' + _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))) + '
    ' + pglist + '
    ' ) vbanner = ('
    ✓ %s
    ' % ( _L("도메인 소유 인증 완료(%s) — 심화 진단 포함", "Domain ownership verified (%s) — 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 = ('
    ' '
    ' '
    ' '
    C H I T O S · ' + kind + '
    ' '
    ' + _L("보안 · SEO · AI가시성 진단 리포트", "Security · SEO · AI-Visibility Report", lang) + '
    ' '
    ' + _L("대상", "Target", lang) + ' ' + html.escape(host) + ' · ' + today + ' · ' + _L("비침투(읽기전용)", "non-intrusive (read-only)", lang) + '
    ' + _ring(pct, grade, lang, dark=True) + '
    ' + vbanner + '
    ' + _scorebar(bS, d['sec_grade']) + _scorebar(bSEO, d.get('seo_grade', ['-', 0])) + _scorebar(bAI, d['geo_grade']) + '
    ' + _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) + '
    ' + '
    ' + '
    %d
    %s
    ' % (nhi, _L("높음", "High", lang)) + '
    %d
    %s
    ' % (nmid, _L("보완", "Advisory", lang)) + '
    %d
    %s
    ' % (nlow, _L("낮음", "Low", lang)) + '
    ' + _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)) + '
    ' + _L("생성형 AI가 당신을 인용하려면 로봇이 먼저 들어와야 합니다.", "For generative AI to cite you, its crawlers must get in first.", lang) + '
    ' + _traffic(d.get('matrix', {}), lang) + _cards(geo, _L("✓ 기계 판독 신호 양호", "✓ Machine-readability signals are healthy", lang), lang) + _checklist(d.get('checks', {}).get('geo', []), lang) + '
    ' + _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)) + '%
    ' + deep_html + (_upsell(lang) if show_upsell else "") + _h2("✓ " + _L("잘 되어 있는 점", "What's done well", lang)) + '
    ' + good + '
    ' + _h2("📌 " + _L("조치 우선순위", "Action priorities", lang)) + _priority(allf, lang) + _consult_cta(nhi, nmid, nlow, lang) + '
    ' + _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) + '
    ' + '
    ') return body def _save_html(body, host, deep): full = "CHITOS %s%s" % (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 ("""
    """ + _L("치토스가 점검 중입니다...", "CHITOS is scanning...", lang) + """
    """ + _L("보안 · SEO · AI 가시성을 읽는 중 (약 10~25초)", "Reading security · SEO · AI visibility (about 10–25s)", lang) + """
    """) 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('먼저 위의 "Sign in with Hugging Face" 버튼으로 로그인하세요. (회사/조직 도메인 메일로 가입한 계정)', 'First sign in with the "Sign in with Hugging Face" 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("도메인 소유 확인 실패. 진단 대상(%s)의 도메인 이메일로 가입한 HF 계정으로 로그인해야 합니다. 현재 계정 도메인: %s" % (html.escape(tdom), html.escape(edom)), "Ownership check failed. Sign in with an HF account on the target domain (%s). Your account domain: %s" % (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('
  • %s
  • ' % b for b in bullets) return ('
    ' '
    %s %s
    ' '
    %s
    ' '' '
    %s
    ') % (step, title, lead, lis, foot) def desc1(lang): if lang == "ko": return _steplead("①", "무료 즉시 진단 (FREE)", "주소만 넣으면 약 10~25초 안에 공개적으로 드러난 보안·검색·AI 노출 위생을 비침투(읽기전용)로 점검합니다. 공격·부하·로그인 없이 홈 1페이지를 읽어 핵심만 요약합니다.", ["보안 위생 — HTTPS 강제·HSTS, 보안 응답 헤더(CSP·X-Frame-Options 등), 쿠키 속성, 서버 버전 노출, 민감·백업 파일 노출", "SEO 토대 — 제목·메타설명·canonical·구조화데이터·이미지 대체텍스트 등 검색 노출의 기본기", "AI 가시성(GEO) — 생성형 AI·검색 로봇 9종(Googlebot·Bingbot·네이버 Yeti·GPTBot·ClaudeBot·PerplexityBot·OAI-SearchBot 등) 도달성", "등급 카드 — 보안·SEO·AI가시성 3축 점수 + 가장 급한 상위 이슈"], "본 단계는 누구나·대상 제한 없이 가능하며 대상 서비스에 영향을 주지 않습니다(읽기전용).") return _steplead("①", "Free Instant Scan", "Enter a URL and within ~10–25s CHITOS checks publicly visible security, search and AI-exposure hygiene non-intrusively (read-only) — no attack, no load, no login. It reads one home page and summarizes the essentials.", ["Security hygiene — forced HTTPS/HSTS, security response headers (CSP, X-Frame-Options, …), cookie flags, server-version exposure, sensitive/backup file exposure", "SEO foundation — title, meta description, canonical, structured data, image alt-text and other search basics", "AI visibility (GEO) — reachability of 9 generative-AI/search crawlers (Googlebot, Bingbot, Naver Yeti, GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot, …)", "Grade cards — 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("②", "도메인 인증 심화 진단", "회사/조직 도메인 이메일로 가입한 Hugging Face 계정으로 로그인하면, 이메일 도메인과 진단 대상 도메인이 일치하는지로 소유를 확인합니다(별도 인증코드 없음). 소유가 확인되면 전사이트 다중 페이지를 크롤링한 정밀 리포트가 열립니다. 여전히 비침투입니다.", ["전사이트 크롤 — 사이트맵 기반 다중 페이지 점검(페이지별 이슈·색인차단·canonical·얇은 페이지)", "구조화데이터 커버리지 — Organization·Product·Service·FAQ 등 Schema 보유/누락", "이메일 위조방지 — SPF·DMARC 레코드 점검(사칭·피싱 방어)", "자기모순 탐지 — 주소·전화·사업자번호가 페이지마다 다른지(AI 신뢰도 핵심)", "조치 우선순위 + 다운로드 리포트(HTML)"], "소유 인증은 로그인 계정의 인증된 이메일 도메인으로만 판정합니다. 타인 사이트 무단 정밀점검은 불가합니다.") return _steplead("②", "Domain-Verified Deep Scan", "Sign in with a Hugging Face account registered on your company/org domain email. 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 non-intrusive.", ["Full-site crawl — sitemap-based multi-page checks (per-page issues, noindex, canonical, thin pages)", "Structured-data coverage — Organization/Product/Service/FAQ schema present or missing", "Email anti-spoofing — SPF/DMARC record checks (impersonation/phishing defense)", "Self-contradiction detection — address/phone/business-number differing across pages (key for AI trust)", "Action priorities + downloadable report (HTML)"], "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("③", "정밀 진단 상담", "비침투 스캔 너머의 실제 취약점과 AI 시대 심층 진단은 전문가가 서면 허가 기반으로 수행합니다. 아래에서 진단 분야를 선택하고 신청하세요.", ["SQL 삽입·파일 업로드·인증/세션 우회 등 실제 웹 취약점 능동 점검(허가 후)", "소스 코드 취약점 점검(시큐어코딩)", "금융보안원 「금융분야 AI 에이전트 보안 평가기준」(6개 분야 17항목) 대응 레드티밍"], "능동 점검은 반드시 대상 소유자의 서면 허가 범위 안에서만 수행합니다.") return _steplead("③", "In-Depth Consultation", "Beyond non-intrusive scanning, real vulnerabilities and AI-era deep diagnosis are performed by experts under written authorization. Pick a diagnosis area below and request a consult.", ["Active testing of real web vulnerabilities — 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 = '
    ' for t, dsc in cards: grid += ('
    %s' '
    %s
    ') % (t, dsc) grid += '
    ' return '
    ' + head + '
    ' + grid def _artex_section(lang): if lang == "ko": head = "🚨 왜 지금인가 — 에이전트형(agentic) 공격의 부상" bullets = ["AI 침투 에이전트가 정찰→침투→탈취를 사람 개입 없이 자동으로 수행하는 공격이 현실화", "2026년 오픈소스 에이전트 ‘ARTEX’가 실제 공격에 동원 — Microsoft 365 2,250여 테넌트 공격, 전자상거래 주문 DB 침해", "공통 진입점은 인터넷에 노출된 미패치 시스템과 약한 웹·인프라 위생 — 전 세계·전 산업 공통", "공격 표면 = 웹·인프라 위생 + AI 에이전트 권한·도구 오남용 (이중용도 도구의 악용)"] resp = "대응: 웹서비스 · AX 인프라 · AI 모델 · AI 에이전트(AX-RAY) 4대 진단으로 공격·방어 양면을 점검합니다." src = "참고(2026): therecord.media · bankinfosecurity · techtimes.com" else: head = "🚨 Why now — the rise of agentic attacks" bullets = ["AI penetration agents now run recon → breach → exfiltration autonomously, with no human in the loop", "In 2026 the open-source agent “ARTEX” was used in real attacks — hitting 2,250+ Microsoft 365 tenants and breaching e-commerce order databases", "The common entry point is unpatched internet-facing systems and weak web/infra hygiene — across industries worldwide", "Attack surface = web/infra hygiene + AI-agent privilege & tool misuse (abuse of a dual-use tool)"] resp = "Response: four diagnoses — Web Service · AX Infra · AI Model · AI Agent (AX-RAY) — covering both attack and defense." src = "References (2026): therecord.media · bankinfosecurity · techtimes.com" lis = "".join('
  • %s
  • ' % b for b in bullets) return ('
    ' '' + head + '' '' '
    🛡 ' + resp + '
    ' '
    ' + src + '
    ') def guideline_html(lang): ko = (lang == "ko") def chk(items): return "".join('
    ☐ %s
    ' % x for x in items) row = lambda a, dom, intl: ('%s' '%s' '%s') % (a, dom, intl) if ko: hero_t = "국내외 보안·AI 안전 기준 수용 현황 · 자가 점검 체크리스트" hero_s = "치토스 진단은 아래 공개 기준을 참조·반영합니다. (내부 점검 로직·공격셋·임계값은 비공개)" h_map = "🧭 진단 분야 × 대표 준거 매핑" th = ("진단 분야", "국내 기준", "국제 기준") rows = [row("웹서비스
    보안진단", "KISA 웹 취약점 진단·제거 가이드 · KISA 소프트웨어 보안약점(시큐어코딩) 가이드 · ISMS-P", "OWASP Top 10 · OWASP ASVS · NIST SSDF(SP 800-218)"), row("AI 모델
    안전진단", "과기정통부·TTA AI 신뢰성/안전 지침 · 금융보안원 AI 평가기준(일부)", "OWASP Top 10 for LLM Apps · NIST AI RMF · MITRE ATLAS · EU AI Act · ISO/IEC 42001"), row("AX 인프라
    보안진단", "ISMS-P · 개인정보 안전성 확보조치 기준 · 전자금융감독규정", "ISO/IEC 27001 · NIST SSDF · OWASP ASVS · Google SAIF"), row("AI 에이전트
    보안진단", "금융보안원 「금융분야 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 = "본 체크리스트는 공개 베스트프랙티스 요약입니다. 치토스 정밀 진단은 각 항목을 실제 환경에서 측정·검증하고 항목별 처방을 제공합니다. 점검 방법·공격셋·판정 기준 등 내부 방법론은 비공개입니다. 능동 점검은 서면 허가 범위 안에서만 수행합니다." svc = "서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)" else: hero_t = "Domestic & international security / AI-safety standards we incorporate · self-check checklist" hero_s = "CHITOS diagnosis references and reflects 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
    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
    Security", "ISMS-P · Personal-data safeguards · E-Financial Supervisory Rules", "ISO/IEC 27001 · NIST SSDF · OWASP ASVS · Google SAIF"), row("AI Agent
    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 measures and verifies each item in the real environment and gives per-item remediation. Internal methodology (methods, attack sets, decision thresholds) is confidential. Active testing is performed only within written-authorization scope." svc = "Service: QuantID · Engine: CHITOS" html_out = ( '
    ' '
    ' '
    C H I T O S · ' + _L("준거 체계", "Standards", lang) + '
    ' '
    ' + hero_t + '
    ' '
    ' + hero_s + '
    ' + _artex_section(lang) + _h2(h_map) + '' '' '' '' + "".join(rows) + '
    ' + th[0] + '' + th[1] + '' + th[2] + '
    ' + '
    ' + note + '
    ' + _h2(h_all) + '
    ' '
    ' + dom_t + '' '
      ' + "".join("
    • %s
    • " % x for x in dom_list) + '
    ' '
    ' + intl_t + '' '
      ' + "".join("
    • %s
    • " % x for x in intl_list) + '
    ' + _h2(h_chk) + '
    ' + '
    ' + c1_t + '' + chk(c1) + '
    ' + '
    ' + c2_t + '' + chk(c2) + '
    ' + '
    ' + c3_t + '' + chk(c3) + '
    ' + '
    ' + c4_t + '' + chk(c4) + '
    ' + '
    ' + '
    ' + foot + '
    ' + '
    ' + svc + '
    ' + '
    ') return html_out def threat_banner(lang): if lang == "ko": t = ("⚠ AI 에이전트가 스스로 침투하는 시대입니다. 2026년 오픈소스 AI 침투 에이전트 ‘ARTEX’가 실제 표적에 동원되어 " "Microsoft 365 2,250여 테넌트 공격, 전자상거래 주문 DB 침해 등 인터넷에 노출된 미패치 시스템을 정찰부터 탈취까지 자동으로 수행했습니다. " "공격이 자동화된 지금, 방어도 자동화되어야 합니다 — 웹·인프라·AI 모델·AI 에이전트를 함께 진단하세요.") else: t = ("⚠ AI agents now break in on their own. In 2026 the open-source AI penetration agent “ARTEX” was turned on real targets — " "hitting 2,250+ Microsoft 365 tenants, breaching e-commerce order databases, and automating recon-to-exfiltration against " "unpatched internet-facing systems worldwide. Attacks are automated now — so must defense be. Diagnose your web, infra, AI models and AI agents together.") return ('
    ' + t + '
    ') def header_html(lang): return ('
    C H I T O S
    ' '

    ' + _L("보안 · SEO · AI가시성 진단", "Security · SEO · AI-Visibility Diagnosis", lang) + '

    ' '
    ' + _L("① 무료 즉시 리포트 → ② 도메인 메일 HF 로그인 심화 리포트 → ③ 정밀 진단 상담 · ④ 준거·체크리스트", "① Free instant report → ② Domain-email HF login deep report → ③ In-depth consult · ④ Standards & checklist", lang) + '
    ') def footer_html(lang): return '
    ' + _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) + '
    ' 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()