File size: 71,263 Bytes
020204d 3658b75 7e3f615 3658b75 32c40dd 3658b75 bece9a3 7e3f615 020204d 774e6c9 020204d 774e6c9 020204d 774e6c9 020204d 3658b75 020204d bece9a3 020204d 7e3f615 020204d 774e6c9 020204d 7e3f615 020204d 7e3f615 020204d 3658b75 020204d 7e3f615 020204d d38bbef 3658b75 020204d d38bbef 020204d d38bbef 020204d d38bbef 020204d d38bbef 020204d d38bbef 020204d e5e842f 90f8f5a e5e842f 90f8f5a e5e842f 90f8f5a e5e842f 90f8f5a e5e842f 020204d 32c40dd 020204d 774e6c9 e5e842f 020204d e5e842f 90f8f5a e5e842f 90f8f5a 32c40dd e5e842f 020204d d38bbef 020204d 3658b75 d38bbef e5e842f d38bbef 020204d 32c40dd 020204d d38bbef 020204d e5e842f 940fac2 020204d d38bbef 020204d d38bbef 020204d d38bbef e5e842f d38bbef 020204d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 | # -*- 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()
|