SeaWolf-AI commited on
Commit
ed41730
·
verified ·
1 Parent(s): 6390ca9

UI 엔진연결(gradio_client) 반영

Browse files
Files changed (2) hide show
  1. app.py +200 -191
  2. requirements.txt +3 -2
app.py CHANGED
@@ -1,191 +1,200 @@
1
- # -*- coding: utf-8 -*-
2
- """치토스(CHITOS) 인스턴트 스캔 — 공개 UI(얇은 클라이언트).
3
- 점검 로직·채점은 비공개 백엔드(치토스 서버)에서 수행 → 이 공개 소스엔 방법이 없습니다.
4
- 2단계 인증: 회사/조직 도메인 메일로 가입한 Hugging Face 계정으로 로그인 → 이메일 도메인이
5
- 진단 대상과 일치하면 심화 진단 승인(별도 코드 발송 없음).
6
- 서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)."""
7
- import os, html
8
- from urllib.parse import urlparse
9
- import requests
10
- import gradio as gr
11
- from huggingface_hub import whoami
12
-
13
- BACKEND = os.environ.get("CHITOS_BACKEND", "").rstrip("/")
14
- APIKEY = os.environ.get("CHITOS_API_KEY", "")
15
- CONSULT = os.environ.get("CHITOS_CONSULT_ENDPOINT", "")
16
-
17
- GCOL = {"A+": "#067647", "A": "#067647", "B": "#2563eb", "C": "#b54708", "D": "#b54708", "F": "#b42318"}
18
- SEV = {"hi": ("#b42318", "#fef3f2", "높음"), "mid": ("#b54708", "#fffaeb", "보완"), "low": ("#8a6d00", "#fdfce8", "낮음")}
19
-
20
-
21
- def _api(path, payload):
22
- if not BACKEND:
23
- return {"error": "백엔드가 아직 연결되지 않았습니다(운영 설정 대기)."}
24
- try:
25
- r = requests.post(BACKEND + path, json=payload, headers={"X-Chitos-Key": APIKEY}, timeout=60)
26
- return r.json()
27
- except Exception as e:
28
- return {"error": "진단 서버 연결 실패: %s" % str(e)[:80]}
29
-
30
-
31
- def _domain(u):
32
- u = (u or "").strip()
33
- if "://" not in u:
34
- u = "https://" + u
35
- return (urlparse(u).hostname or "").replace("www.", "")
36
-
37
-
38
- def _grade_badge(g, label, sub):
39
- letter, score = g[0], g[1]
40
- col = GCOL.get(letter, "#b54708")
41
- return ('<div style="flex:1;text-align:center;border:1px solid #e4e9ef;border-radius:14px;padding:18px">'
42
- '<div style="font-size:13px;color:#6b7280;font-weight:700">%s</div>'
43
- '<div style="font-size:52px;font-weight:900;line-height:1;color:%s;margin:6px 0">%s</div>'
44
- '<div style="font-size:12px;color:#9ca3af">%s / 100 · %s</div></div>') % (label, col, letter, score, sub)
45
-
46
-
47
- def _cards(items, empty):
48
- if not items:
49
- return '<div style="color:#067647;font-weight:700;padding:8px 0">%s</div>' % empty
50
- out = ""
51
- for sev, title, detail in items:
52
- c, bg, lab = SEV.get(sev, SEV["low"])
53
- out += ('<div style="border:1px solid #e4e9ef;border-left:4px solid %s;border-radius:9px;padding:10px 13px;margin:7px 0;background:%s">'
54
- '<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(str(title)), html.escape(str(detail)))
55
- return out
56
-
57
-
58
- def _traffic(matrix):
59
- rows = ""
60
- for name, code in matrix.items():
61
- ok = code == "200"
62
- dot = "#16a34a" if ok else ("#dc2626" if code in ("403", "401", "429", "차단") else "#9ca3af")
63
- txt = "정상" if ok else ("차단" if code in ("403", "401", "429", "차단") else code)
64
- rows += ('<div style="display:flex;align-items:center;gap:9px;padding:6px 10px;border:1px solid #eef1f6;border-radius:8px">'
65
- '<span style="width:11px;height:11px;border-radius:50%%;background:%s;flex:none"></span>'
66
- '<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)
67
- return '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:7px">%s</div>' % rows
68
-
69
-
70
- def _note(msg, sev="ok"):
71
- c = {"ok": "#067647", "mid": "#b54708", "hi": "#b42318"}.get(sev, "#067647")
72
- bg = {"ok": "#ecfdf3", "mid": "#fffaeb", "hi": "#fef3f2"}.get(sev, "#ecfdf3")
73
- 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)
74
-
75
-
76
- def _render(d, deep=False):
77
- if d.get("error"):
78
- return _note("⚠ " + html.escape(d["error"]), "hi")
79
- if d.get("blocked"):
80
- return ('<div style="font-family:sans-serif;max-width:700px;margin:0 auto;border:1px solid #e4e9ef;border-radius:12px;padding:20px">'
81
- '<h3 style="color:#067647">🛡 이 사이트는 자동 공격 차단(WAF)이 작동 중입니다</h3>'
82
- '<p>비정상 접근을 탐지해 차단했습니다. <b>방어가 잘 되어 있다는 좋은 신호</b>입니다. 치토스는 차단을 우회하지 않습니다(읽기전용). 정밀 점검은 소유자 허가 후 진행합니다.</p></div>')
83
- meta = d.get("meta", {})
84
- 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(str(g)) for g in d.get("good", [])) or "—"
85
- deep_html = ""
86
- if deep and d.get("deep"):
87
- dp = d["deep"]
88
- deep_html = f"""<h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px;margin-top:20px">🔎 심화: 다중 페이지 ({dp['pages']}개)</h3>
89
- <ul style="font-size:13px;color:#374151;line-height:1.7">
90
- <li>제목 <b>{dp['uniq_titles']}종 고유 / {dp['dup_titles']}개 중복</b> {'⚠ 중복 많음' if dp['dup_titles']>0 else '✓'}</li>
91
- <li>대제목(H1) 합계 <b>{dp['h1_total']}개</b></li>
92
- <li>구조화 데이터 합계 <b>{dp['jsonld_total']}개</b> {'⚠ 서비스·FAQ 추가 권장' if dp['jsonld_total']<2 else '✓'}</li>
93
- </ul>"""
94
- return f"""
95
- <div style="font-family:'Malgun Gothic',sans-serif;max-width:860px;margin:0 auto">
96
- <div style="display:flex;gap:14px;margin-bottom:6px">
97
- {_grade_badge(d['sec_grade'],"🛡 보안 등급","자물쇠 "+str(d.get('locks_on',0))+"/6 켜짐")}
98
- {_grade_badge(d['geo_grade'],"🔍 AI 가시성(GEO)","검색·AI 노출")}
99
- </div>
100
- <div style="font-size:12.5px;color:#6b7280;text-align:center;margin-bottom:16px">대상 <b>{html.escape(str(d.get('host','')))}</b> · 비침투(읽기전용) · 치토스(CHITOS)</div>
101
- <h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px">🛡 보안 점검</h3>
102
- {_cards(d.get('sec',[]), "✓ 외부에서 보이는 보안 위생 문제 없음")}
103
- <h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px;margin-top:20px">🤖 AI 로봇이 들어올 수 있나</h3>
104
- {_traffic(d.get('matrix',{}))}
105
- <h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px;margin-top:20px">🔍 검색·AI 가시성(GEO)</h3>
106
- {_cards(d.get('geo',[]), "✓ 기계 판독 신호 양호")}
107
- <div style="font-size:12px;color:#6b7280;margin-top:6px">제목: {html.escape(str(meta.get('title') or '(없음)'))} · H1 {meta.get('h1',0)}개 · 구조화 {meta.get('jsonld',0)}개 · 홈 {meta.get('size_kb',0)}KB</div>
108
- {deep_html}
109
- <h3 style="color:#067647;margin-top:20px">✓ 잘 되어 있는 점</h3><div>{good}</div>
110
- <div style="background:#f4f0ff;border:1px dashed #b89cff;border-radius:12px;padding:14px 16px;margin-top:18px">
111
- <b style="color:#6d28d9">더 깊은 점검이 필요하다면?</b><br>
112
- <span style="font-size:13px;color:#374151">이 진단은 <b>비침투(읽기전용)</b>입니다. SQL삽입·파일업로드·인증우회 등 실제 취약점과 금융보안원 17종 에이전트 보안 정밀 점검, 전체 PDF 리포트는 <b>3단계 상담</b>으로 제공합니다.</span>
113
- </div>
114
- <div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS) · 비침투·비파괴</div>
115
- </div>"""
116
-
117
-
118
- def free_scan(url):
119
- if not (url or "").strip():
120
- return _note("진단할 주소를 입력하세요. 예: example.com", "mid")
121
- return _render(_api("/scan", {"url": url}))
122
-
123
-
124
- def deep_scan(url, profile: gr.OAuthProfile | None = None, oauth_token: gr.OAuthToken | None = None):
125
- if profile is None or oauth_token is None:
126
- return _note('먼저 위의 <b>"Hugging Face로 로그인"</b> 버튼으로 로그인하세요. (회사/조직 도메인 메일로 가입한 계정)', "mid")
127
- if not (url or "").strip():
128
- return _note("진단할 주소를 입력하세요.", "mid")
129
- try:
130
- me = whoami(token=oauth_token.token)
131
- email = (me.get("email") or "").lower()
132
- except Exception:
133
- email = ""
134
- if not email:
135
- return _note("로그인 계정의 <b>인증된 이메일</b>을 읽지 못했습니다. HF 계정 이메일을 확인·인증 후 다시 시도하세요.", "hi")
136
- edom = email.split("@")[-1]
137
- tdom = _domain(url)
138
- if edom != tdom and not tdom.endswith("." + edom) and not edom.endswith("." + tdom):
139
- return _note("도메인 소유 확인 실패. 진단 대상(<b>%s</b>)의 도메인 이메일로 가입한 HF 계정으로 로그인해야 합니다. 현재 계정 이메일 도메인: <b>%s</b>" % (html.escape(tdom), html.escape(edom)), "hi")
140
- d = _api("/deep", {"url": url})
141
- pre = _note("✓ 도메인 소유 인증 완료(<b>%s</b>) — 심화 진단을 수행했습니다. (비침투)" % html.escape(email), "ok")
142
- return pre + _render(d, deep=True)
143
-
144
-
145
- def consult(name, company, email, phone, msg):
146
- if not (name and email):
147
- return _note("이름과 이메일은 필수입니다.", "mid")
148
- if not CONSULT:
149
- return _note("상담 접수처가 아직 설정되지 않았습니다. 잠시 후 다시 시도해 주세요.", "mid")
150
- try:
151
- rr = requests.post(CONSULT, json={"이름": name, "회사": company or "-", "이메일": email,
152
- "연락처": phone or "-", "문의": msg or "-", "_subject": "[CHITOS] 정밀진단 상담신청"}, timeout=15)
153
- return _note("상담 신청이 접수됐습니다. 퀀트ID(QuantID)에서 곧 연락드립니다. 감사합니다.", "ok") if rr.status_code < 300 else _note("접수 중 문제가 발생했습니다. 다시 시도해 주세요.", "mid")
154
- except Exception:
155
- return _note("접수 연결 실패. 잠시 후 다시 시도해 주세요.", "mid")
156
-
157
-
158
- CSS = "#title{text-align:center}.gradio-container{max-width:980px!important;margin:0 auto}"
159
- with gr.Blocks(title="CHITOS 인스턴트 스캔", css=CSS, theme=gr.themes.Soft(primary_hue="purple")) as demo:
160
- gr.HTML('<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>'
161
- '<h1 style="margin:6px 0;color:#2e1065">보안 · AI가시성 진단</h1>'
162
- '<div style="color:#6b7280">① 무료 즉시 진단 → ② 도메인 메일 HF 로그인 심화 진단 → ③ 정밀 진단 상담</div></div>')
163
- with gr.Tabs():
164
- with gr.Tab("① 무료 즉시 진단 (FREE)"):
165
- gr.HTML('<div style="color:#4b5563;font-size:13px;margin:4px 0 8px">주소만 넣으세요. 공격 없이(읽기전용) 보안 등급과 "AI가 당신을 읽을 수 있는지"를 바로.</div>')
166
- with gr.Row():
167
- u1 = gr.Textbox(label="", placeholder="example.com", scale=4, container=False)
168
- b1 = gr.Button("무료 진단", variant="primary", scale=1)
169
- o1 = gr.HTML()
170
- b1.click(free_scan, u1, o1); u1.submit(free_scan, u1, o1)
171
- with gr.Tab("② 도메인 인증 심화 진단"):
172
- gr.HTML('<div style="color:#4b5563;font-size:13px;margin:4px 0 8px"><b>회사/조직 도메인 이메일로 가입한 Hugging Face 계정</b>으로 로그인하세요. '
173
- '그 이메일 도메인이 진단 대상과 일치하면(소유 확인) 다중 페이지 심화 진단이 열립니다. 별도 인증코드 발송은 없습니다.</div>')
174
- gr.LoginButton()
175
- u2 = gr.Textbox(label="진단할 주소", placeholder="example.com")
176
- b2 = gr.Button("인증 후 심화 진단", variant="primary")
177
- o2 = gr.HTML()
178
- b2.click(deep_scan, inputs=u2, outputs=o2)
179
- with gr.Tab("③ 정밀 진단 상담"):
180
- gr.HTML('<div style="color:#4b5563;font-size:13px;margin:4px 0 8px">금융보안원 17종 에이전트 보안·소스 코드 취약점·능동 점검 등 <b>정밀 진단</b> 상담 신청.</div>')
181
- with gr.Row():
182
- n = gr.Textbox(label="이름*"); co = gr.Textbox(label="회사")
183
- with gr.Row():
184
- em = gr.Textbox(label="이메일*"); ph = gr.Textbox(label="연락처")
185
- mg = gr.Textbox(label="문의 내용", lines=3)
186
- bc = gr.Button("상담 신청", variant="primary"); oc = gr.HTML()
187
- bc.click(consult, [n, co, em, ph, mg], oc)
188
- gr.HTML('<div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">비침투·비파괴 · 타인 사이트 무단 정밀점검 금지 · 능동 점검은 도메인 인증·허가 후 · 서비스 퀀트ID(QuantID) · 엔진 치토스(CHITOS)</div>')
189
-
190
- if __name__ == "__main__":
191
- demo.launch()
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """치토스(CHITOS) 인스턴트 스캔 — 공개 UI(얇은 클라이언트).
3
+ 점검 로직·채점은 비공개 백엔드(치토스 서버)에서 수행 → 이 공개 소스엔 방법이 없습니다.
4
+ 2단계 인증: 회사/조직 도메인 메일로 가입한 Hugging Face 계정으로 로그인 → 이메일 도메인이
5
+ 진단 대상과 일치하면 심화 진단 승인(별도 코드 발송 없음).
6
+ 서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS)."""
7
+ import os, html, json
8
+ from urllib.parse import urlparse
9
+ import requests
10
+ import gradio as gr
11
+ from huggingface_hub import whoami
12
+ from gradio_client import Client
13
+
14
+ ENGINE = os.environ.get("CHITOS_ENGINE_REPO", "")
15
+ HFTOKEN = os.environ.get("HF_TOKEN", "")
16
+ CONSULT = os.environ.get("CHITOS_CONSULT_ENDPOINT", "")
17
+ _eng_client = None
18
+
19
+ def _engine():
20
+ global _eng_client
21
+ if _eng_client is None and ENGINE:
22
+ _eng_client = Client(ENGINE, hf_token=HFTOKEN or None)
23
+ return _eng_client
24
+
25
+ GCOL = {"A+": "#067647", "A": "#067647", "B": "#2563eb", "C": "#b54708", "D": "#b54708", "F": "#b42318"}
26
+ SEV = {"hi": ("#b42318", "#fef3f2", "높음"), "mid": ("#b54708", "#fffaeb", "보완"), "low": ("#8a6d00", "#fdfce8", "낮음")}
27
+
28
+
29
+ def _api(path, payload):
30
+ c = _engine()
31
+ if c is None:
32
+ return {"error": "진단 엔진이 아직 연결되지 않았습니다(운영 설정 대기)."}
33
+ try:
34
+ res = c.predict(payload.get("url", ""), api_name="/" + path.strip("/"))
35
+ return json.loads(res)
36
+ except Exception as e:
37
+ return {"error": "진단 엔진 호출 실패(첫 호출은 기동에 시간이 걸릴 수 있습니다): %s" % str(e)[:90]}
38
+
39
+
40
+ def _domain(u):
41
+ u = (u or "").strip()
42
+ if "://" not in u:
43
+ u = "https://" + u
44
+ return (urlparse(u).hostname or "").replace("www.", "")
45
+
46
+
47
+ def _grade_badge(g, label, sub):
48
+ letter, score = g[0], g[1]
49
+ col = GCOL.get(letter, "#b54708")
50
+ return ('<div style="flex:1;text-align:center;border:1px solid #e4e9ef;border-radius:14px;padding:18px">'
51
+ '<div style="font-size:13px;color:#6b7280;font-weight:700">%s</div>'
52
+ '<div style="font-size:52px;font-weight:900;line-height:1;color:%s;margin:6px 0">%s</div>'
53
+ '<div style="font-size:12px;color:#9ca3af">%s / 100 · %s</div></div>') % (label, col, letter, score, sub)
54
+
55
+
56
+ def _cards(items, empty):
57
+ if not items:
58
+ return '<div style="color:#067647;font-weight:700;padding:8px 0">%s</div>' % empty
59
+ out = ""
60
+ for sev, title, detail in items:
61
+ c, bg, lab = SEV.get(sev, SEV["low"])
62
+ out += ('<div style="border:1px solid #e4e9ef;border-left:4px solid %s;border-radius:9px;padding:10px 13px;margin:7px 0;background:%s">'
63
+ '<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(str(title)), html.escape(str(detail)))
64
+ return out
65
+
66
+
67
+ def _traffic(matrix):
68
+ rows = ""
69
+ for name, code in matrix.items():
70
+ ok = code == "200"
71
+ dot = "#16a34a" if ok else ("#dc2626" if code in ("403", "401", "429", "차단") else "#9ca3af")
72
+ txt = "정상" if ok else ("차단" if code in ("403", "401", "429", "차단") else code)
73
+ rows += ('<div style="display:flex;align-items:center;gap:9px;padding:6px 10px;border:1px solid #eef1f6;border-radius:8px">'
74
+ '<span style="width:11px;height:11px;border-radius:50%%;background:%s;flex:none"></span>'
75
+ '<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)
76
+ return '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:7px">%s</div>' % rows
77
+
78
+
79
+ def _note(msg, sev="ok"):
80
+ c = {"ok": "#067647", "mid": "#b54708", "hi": "#b42318"}.get(sev, "#067647")
81
+ bg = {"ok": "#ecfdf3", "mid": "#fffaeb", "hi": "#fef3f2"}.get(sev, "#ecfdf3")
82
+ 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)
83
+
84
+
85
+ def _render(d, deep=False):
86
+ if d.get("error"):
87
+ return _note("⚠ " + html.escape(d["error"]), "hi")
88
+ if d.get("blocked"):
89
+ return ('<div style="font-family:sans-serif;max-width:700px;margin:0 auto;border:1px solid #e4e9ef;border-radius:12px;padding:20px">'
90
+ '<h3 style="color:#067647">🛡 이 사이트는 자동 공격 차단(WAF)이 작동 중입니다</h3>'
91
+ '<p>비정상 접근을 탐지해 차단했습니다. <b>방어가 잘 되어 있다는 좋은 신호</b>입니다. 치토스는 차단을 우회하지 않습니다(읽기전용). 정밀 점검은 소유자 허가 후 진행합니다.</p></div>')
92
+ meta = d.get("meta", {})
93
+ 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(str(g)) for g in d.get("good", [])) or "—"
94
+ deep_html = ""
95
+ if deep and d.get("deep"):
96
+ dp = d["deep"]
97
+ deep_html = f"""<h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px;margin-top:20px">🔎 심화: 다중 페이지 ({dp['pages']}개)</h3>
98
+ <ul style="font-size:13px;color:#374151;line-height:1.7">
99
+ <li>제목 <b>{dp['uniq_titles']}종 고유 / {dp['dup_titles']}개 중복</b> {'⚠ 중복 많음' if dp['dup_titles']>0 else '✓'}</li>
100
+ <li>대제목(H1) 합계 <b>{dp['h1_total']}개</b></li>
101
+ <li>구조화 데이터 합계 <b>{dp['jsonld_total']}개</b> {'⚠ 서비스·FAQ 추가 권장' if dp['jsonld_total']<2 else '✓'}</li>
102
+ </ul>"""
103
+ return f"""
104
+ <div style="font-family:'Malgun Gothic',sans-serif;max-width:860px;margin:0 auto">
105
+ <div style="display:flex;gap:14px;margin-bottom:6px">
106
+ {_grade_badge(d['sec_grade'],"🛡 보안 등급","자물쇠 "+str(d.get('locks_on',0))+"/6 켜짐")}
107
+ {_grade_badge(d['geo_grade'],"🔍 AI 가시성(GEO)","검색·AI 노출")}
108
+ </div>
109
+ <div style="font-size:12.5px;color:#6b7280;text-align:center;margin-bottom:16px">대상 <b>{html.escape(str(d.get('host','')))}</b> · 비침투(읽기전용) · 치토스(CHITOS)</div>
110
+ <h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px">🛡 보안 점검</h3>
111
+ {_cards(d.get('sec',[]), "✓ 외부에서 보이는 보안 위생 문제 없음")}
112
+ <h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px;margin-top:20px">🤖 AI 로봇이 들어올 수 있나</h3>
113
+ {_traffic(d.get('matrix',{}))}
114
+ <h3 style="color:#2e1065;border-bottom:2px solid #6d28d9;padding-bottom:6px;margin-top:20px">🔍 검색·AI 가시성(GEO)</h3>
115
+ {_cards(d.get('geo',[]), "✓ 기계 판독 신호 양호")}
116
+ <div style="font-size:12px;color:#6b7280;margin-top:6px">제목: {html.escape(str(meta.get('title') or '(없음)'))} · H1 {meta.get('h1',0)}개 · 구조화 {meta.get('jsonld',0)}개 · 홈 {meta.get('size_kb',0)}KB</div>
117
+ {deep_html}
118
+ <h3 style="color:#067647;margin-top:20px">✓ 잘 되어 있는 점</h3><div>{good}</div>
119
+ <div style="background:#f4f0ff;border:1px dashed #b89cff;border-radius:12px;padding:14px 16px;margin-top:18px">
120
+ <b style="color:#6d28d9">더 깊은 점검이 필요하다면?</b><br>
121
+ <span style="font-size:13px;color:#374151">이 진단은 <b>비침투(읽기전용)</b>입니다. SQL삽입·파일업로드·인증우회 등 실제 취약점과 금융보안원 17종 에이전트 보안 정밀 점검, 전체 PDF 리포트는 <b>3단계 상담</b>으로 제공합니다.</span>
122
+ </div>
123
+ <div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS) · 비침투·비파괴</div>
124
+ </div>"""
125
+
126
+
127
+ def free_scan(url):
128
+ if not (url or "").strip():
129
+ return _note("진단할 주소를 입력하세요. 예: example.com", "mid")
130
+ return _render(_api("/scan", {"url": url}))
131
+
132
+
133
+ def deep_scan(url, profile: gr.OAuthProfile | None = None, oauth_token: gr.OAuthToken | None = None):
134
+ if profile is None or oauth_token is None:
135
+ return _note('먼저 위의 <b>"Hugging Face로 로그인"</b> 버튼으로 로그인하세요. (회사/조직 도메인 메일로 가입한 계정)', "mid")
136
+ if not (url or "").strip():
137
+ return _note("진단할 주소를 입력하세요.", "mid")
138
+ try:
139
+ me = whoami(token=oauth_token.token)
140
+ email = (me.get("email") or "").lower()
141
+ except Exception:
142
+ email = ""
143
+ if not email:
144
+ return _note("로그인 계정의 <b>인증된 이메일</b>을 읽지 못했습니다. HF 계정 이메일을 확인·인증 후 다시 시도하세요.", "hi")
145
+ edom = email.split("@")[-1]
146
+ tdom = _domain(url)
147
+ if edom != tdom and not tdom.endswith("." + edom) and not edom.endswith("." + tdom):
148
+ return _note("도메인 소유 확인 실패. 진단 대상(<b>%s</b>)의 도메인 이메일로 가입한 HF 계정으로 로그인해야 합니다. 현재 계정 이메일 도메인: <b>%s</b>" % (html.escape(tdom), html.escape(edom)), "hi")
149
+ d = _api("/deep", {"url": url})
150
+ pre = _note("✓ 도메인 소유 인증 완료(<b>%s</b>) — 심화 진단을 수행했습니다. (비침투)" % html.escape(email), "ok")
151
+ return pre + _render(d, deep=True)
152
+
153
+
154
+ def consult(name, company, email, phone, msg):
155
+ if not (name and email):
156
+ return _note("이름과 이메일은 필수입니다.", "mid")
157
+ if not CONSULT:
158
+ return _note("상담 접수처가 아직 설정되지 않았습니다. 잠시 후 다시 시도해 주세요.", "mid")
159
+ try:
160
+ rr = requests.post(CONSULT, json={"이름": name, "회사": company or "-", "이메일": email,
161
+ "연락처": phone or "-", "문의": msg or "-", "_subject": "[CHITOS] 정밀진단 상담신청"}, timeout=15)
162
+ return _note("상담 신청이 접수됐습니다. 퀀트ID(QuantID)에서 곧 연락드립니다. 감사합니다.", "ok") if rr.status_code < 300 else _note("접수 중 문제가 발생했습니다. 다시 시도해 주세요.", "mid")
163
+ except Exception:
164
+ return _note("접수 연결 실패. 잠시 후 다시 시도해 주세요.", "mid")
165
+
166
+
167
+ CSS = "#title{text-align:center}.gradio-container{max-width:980px!important;margin:0 auto}"
168
+ with gr.Blocks(title="CHITOS 인스턴트 스캔", css=CSS, theme=gr.themes.Soft(primary_hue="purple")) as demo:
169
+ gr.HTML('<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>'
170
+ '<h1 style="margin:6px 0;color:#2e1065">보안 · AI가시성 진단</h1>'
171
+ '<div style="color:#6b7280">① 무료 즉시 진단 → ② 도메인 메일 HF 로그인 심화 진단 → ③ 정밀 진단 상담</div></div>')
172
+ with gr.Tabs():
173
+ with gr.Tab("① 무료 즉시 진단 (FREE)"):
174
+ gr.HTML('<div style="color:#4b5563;font-size:13px;margin:4px 0 8px">주소만 넣으세요. 공격 없이(읽기전용) 보안 등급과 "AI가 당신을 읽을 수 있는지"를 바로.</div>')
175
+ with gr.Row():
176
+ u1 = gr.Textbox(label="", placeholder="example.com", scale=4, container=False)
177
+ b1 = gr.Button("무료 진단", variant="primary", scale=1)
178
+ o1 = gr.HTML()
179
+ b1.click(free_scan, u1, o1); u1.submit(free_scan, u1, o1)
180
+ with gr.Tab("② 도메인 인증 심화 진단"):
181
+ gr.HTML('<div style="color:#4b5563;font-size:13px;margin:4px 0 8px"><b>회사/조직 도메인 이메일로 가입한 Hugging Face 계정</b>으로 로그인하세요. '
182
+ '그 이메일 도메인이 진단 대상과 일치하면(소유 확인) 다중 페이지 심화 진단이 열립니다. 별도 인증코드 발송은 없습니다.</div>')
183
+ gr.LoginButton()
184
+ u2 = gr.Textbox(label="진단할 주소", placeholder="example.com")
185
+ b2 = gr.Button("인증 후 심화 진단", variant="primary")
186
+ o2 = gr.HTML()
187
+ b2.click(deep_scan, inputs=u2, outputs=o2)
188
+ with gr.Tab("③ 정밀 진단 상담"):
189
+ gr.HTML('<div style="color:#4b5563;font-size:13px;margin:4px 0 8px">금융보안원 17종 에이전트 보안·소스 코드 취약점·능동 점검 등 <b>정밀 진단</b> 상담 신청.</div>')
190
+ with gr.Row():
191
+ n = gr.Textbox(label="이름*"); co = gr.Textbox(label="회사")
192
+ with gr.Row():
193
+ em = gr.Textbox(label="이메일*"); ph = gr.Textbox(label="연락처")
194
+ mg = gr.Textbox(label="문의 내용", lines=3)
195
+ bc = gr.Button("상담 신청", variant="primary"); oc = gr.HTML()
196
+ bc.click(consult, [n, co, em, ph, mg], oc)
197
+ gr.HTML('<div style="text-align:center;color:#9ca3af;font-size:11px;margin-top:12px">비침투·비파괴 · 타인 사이트 무단 정밀점검 금지 · 능동 점검은 도메인 인증·허가 후 · 서비스 퀀트ID(QuantID) · 엔진 치토스(CHITOS)</div>')
198
+
199
+ if __name__ == "__main__":
200
+ demo.launch()
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
- gradio==5.9.1
2
- requests==2.32.3
 
 
1
+ gradio==5.9.1
2
+ requests==2.32.3
3
+ gradio_client==1.5.2