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()