feat: render all-checks checklist (1/2 tabs) + strong consult-conversion CTA (ARTEX framing)
Browse files
app.py
CHANGED
|
@@ -134,6 +134,31 @@ def _metacards(meta, lang):
|
|
| 134 |
return '<div style="display:flex;gap:6px;margin:8px 0">' + cells + '</div>'
|
| 135 |
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
def _upsell(lang):
|
| 138 |
return ('<div class="chdk" style="background:linear-gradient(135deg,#6d28d9,#8b5cf6);border-radius:14px;padding:16px 18px;margin:16px 0">'
|
| 139 |
'<div style="font-weight:800;font-size:15px;color:#fff">🔓 ' + _L("전사이트 정밀 리포트 — ② 도메인 인증 탭", "Full-site report — ② Domain Verification tab", lang) + '</div>'
|
|
@@ -143,6 +168,22 @@ def _upsell(lang):
|
|
| 143 |
+ '</div></div>')
|
| 144 |
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
def _cards(items, empty, lang):
|
| 147 |
if not items:
|
| 148 |
return '<div style="color:#067647;font-weight:700;padding:8px 0">%s</div>' % empty
|
|
@@ -312,6 +353,7 @@ def build_report(d, deep=False, verified_email=None, lang="ko", show_upsell=Fals
|
|
| 312 |
+ _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>'
|
| 313 |
+ _h2("📧 " + _L("이메일 위조 방지", "Email anti-spoofing", lang)) + '<div style="display:flex;gap:10px">' + _pill(es.get("spf"), "SPF") + " " + _pill(es.get("dmarc"), "DMARC") + '</div>'
|
| 314 |
+ incon
|
|
|
|
| 315 |
+ _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>'
|
| 316 |
)
|
| 317 |
|
|
@@ -338,19 +380,17 @@ def build_report(d, deep=False, verified_email=None, lang="ko", show_upsell=Fals
|
|
| 338 |
+ '<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))
|
| 339 |
+ '<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))
|
| 340 |
+ '</div>'
|
| 341 |
-
+ _h2("🛡 " + _L("보안 점검", "Security checks", lang)) + _locks(d.get('locks_on', 0), lang) + _cards(sec, _L("✓ 외부에서 보이는 보안 위생 문제 없음", "✓ No externally visible security-hygiene issues", lang), lang)
|
| 342 |
-
+ _h2("📈 " + _L("SEO 토대 (검색 노출)", "SEO foundation (search visibility)", lang)) + _metacards(meta, lang) + _cards(seo, _L("✓ SEO 기본 요소 양호", "✓ SEO fundamentals are healthy", lang), lang)
|
| 343 |
+ _h2("🤖 " + _L("AI 로봇 접근 (AI 가시성)", "AI crawler access (AI visibility)", lang))
|
| 344 |
+ '<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>'
|
| 345 |
-
+ _traffic(d.get('matrix', {}), lang) + _cards(geo, _L("✓ 기계 판독 신호 양호", "✓ Machine-readability signals are healthy", lang), lang)
|
| 346 |
+ '<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>'
|
| 347 |
+ deep_html
|
| 348 |
+ (_upsell(lang) if show_upsell else "")
|
| 349 |
+ _h2("✓ " + _L("잘 되어 있는 점", "What's done well", lang)) + '<div>' + good + '</div>'
|
| 350 |
+ _h2("📌 " + _L("조치 우선순위", "Action priorities", lang)) + _priority(allf, lang)
|
| 351 |
-
+
|
| 352 |
-
"본 진단은 <b>비침투(읽기전용)</b>입니다. SQL삽입·파일업로드·인증우회 등 실제 취약점과 금융보안원 17종 에이전트 보안 정밀 점검은 <b>상담(3단계)</b>으로 제공합니다.",
|
| 353 |
-
"This diagnosis is <b>non-intrusive (read-only)</b>. Real vulnerabilities (SQLi, file upload, auth bypass) and the FSI 17-item AI-agent security review are provided via <b>Consultation (step 3)</b>.", lang) + '</span></div>'
|
| 354 |
+ '<div style="color:#9ca3af;font-size:11px;margin-top:14px;border-top:1px solid #e4e9ef;padding-top:10px">' + _L(
|
| 355 |
"본 진단은 비침투·비파괴로 수행되었으며 대상 서비스에 영향을 주지 않았습니다. 결과는 측정 시점(" + today + ") 기준. 서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS).",
|
| 356 |
"This diagnosis was non-intrusive and non-destructive and did not affect the target. Results as of " + today + ". Service: QuantID · Engine: CHITOS.", lang) + '</div>'
|
|
|
|
| 134 |
return '<div style="display:flex;gap:6px;margin:8px 0">' + cells + '</div>'
|
| 135 |
|
| 136 |
|
| 137 |
+
def _consult_cta(nhi, nmid, nlow, lang):
|
| 138 |
+
total = nhi + nmid + nlow
|
| 139 |
+
if lang == "ko":
|
| 140 |
+
risk = '<b style="color:#b42318">%d</b>개 개선점(높음 %d · 보완 %d · 낮음 %d)' % (total, nhi, nmid, nlow)
|
| 141 |
+
body = ("이 진단에서 " + risk + "이 확인됐습니다. 하지만 <b>비침투 스캔은 ‘밖에서 보이는 표면’까지</b>입니다 — "
|
| 142 |
+
"SQL삽입·파일업로드·인증우회·IDOR 같은 <b>실제 취약점</b>, 그리고 <b>ARTEX</b>처럼 AI 에이전트가 자동으로 뚫는 위협은 "
|
| 143 |
+
"<b>능동 정밀 진단</b>으로만 확인됩니다.")
|
| 144 |
+
areas = "웹서비스 · AX 인프라 · AI 모델 · AI 에이전트(AX-RAY) 4대 진단 + 금융보안원 17항목 레드티밍"
|
| 145 |
+
go = "👉 <b>③ 정밀 진단 상담</b> 탭에서 분야를 선택해 신청하세요. (서면 허가 기반·국내외 준거 수용)"
|
| 146 |
+
head = "🎯 다음 단계 — 정밀 진단 상담"
|
| 147 |
+
else:
|
| 148 |
+
risk = '<b style="color:#b42318">%d</b> improvement points (High %d · Advisory %d · Low %d)' % (total, nhi, nmid, nlow)
|
| 149 |
+
body = ("This scan found " + risk + ". But a <b>non-intrusive scan only shows the surface</b> — "
|
| 150 |
+
"<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>), "
|
| 151 |
+
"are confirmed only by <b>active in-depth testing</b>.")
|
| 152 |
+
areas = "Web Service · AX Infra · AI Model · AI Agent (AX-RAY) — 4 diagnoses + FSI 17-item red-teaming"
|
| 153 |
+
go = "👉 Request it in the <b>③ In-Depth Consultation</b> tab (authorization-based · aligned to domestic & international standards)."
|
| 154 |
+
head = "🎯 Next step — in-depth consultation"
|
| 155 |
+
return ('<div class="chdk" style="background:linear-gradient(135deg,#4c1d95,#7c3aed);border-radius:16px;padding:18px 20px;margin-top:18px;color:#f3e8ff">'
|
| 156 |
+
'<div style="font-size:16px;font-weight:800;color:#fff">' + head + '</div>'
|
| 157 |
+
'<div style="font-size:13px;margin-top:8px;line-height:1.55">' + body + '</div>'
|
| 158 |
+
'<div style="background:#ffffff1f;border-radius:10px;padding:9px 13px;margin-top:10px;font-size:12.5px;color:#fff">🛡 ' + areas + '</div>'
|
| 159 |
+
'<div style="font-size:13px;margin-top:10px;font-weight:700;color:#fff">' + go + '</div></div>')
|
| 160 |
+
|
| 161 |
+
|
| 162 |
def _upsell(lang):
|
| 163 |
return ('<div class="chdk" style="background:linear-gradient(135deg,#6d28d9,#8b5cf6);border-radius:14px;padding:16px 18px;margin:16px 0">'
|
| 164 |
'<div style="font-weight:800;font-size:15px;color:#fff">🔓 ' + _L("전사이트 정밀 리포트 — ② 도메인 인증 탭", "Full-site report — ② Domain Verification tab", lang) + '</div>'
|
|
|
|
| 168 |
+ '</div></div>')
|
| 169 |
|
| 170 |
|
| 171 |
+
def _checklist(items, lang):
|
| 172 |
+
if not items:
|
| 173 |
+
return ""
|
| 174 |
+
ic = {"pass": ("✓", "#16a34a", "#f0fdf4"), "warn": ("!", "#b54708", "#fffaeb"), "fail": ("✕", "#b42318", "#fef2f2")}
|
| 175 |
+
npass = len([i for i in items if i[0] == "pass"])
|
| 176 |
+
cells = ""
|
| 177 |
+
for st, lab in items:
|
| 178 |
+
sym, col, bg = ic.get(st, ic["warn"])
|
| 179 |
+
cells += ('<div style="display:flex;gap:8px;align-items:center;padding:5px 9px;border:1px solid #eef1f6;border-radius:7px;background:%s">'
|
| 180 |
+
'<span style="color:%s;font-weight:900;width:13px;text-align:center;flex:none">%s</span>'
|
| 181 |
+
'<span style="font-size:12px;color:#374151">%s</span></div>') % (bg, col, sym, html.escape(_P(lab, lang)))
|
| 182 |
+
head = (_L("점검 항목 전체", "All checks", lang) + ' · <b style="color:#16a34a">%d/%d %s</b>' % (npass, len(items), _L("통과", "pass", lang)))
|
| 183 |
+
return ('<div style="font-size:11.5px;color:#6b7280;margin:8px 0 5px">📋 ' + head + '</div>'
|
| 184 |
+
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:6px">' + cells + '</div>')
|
| 185 |
+
|
| 186 |
+
|
| 187 |
def _cards(items, empty, lang):
|
| 188 |
if not items:
|
| 189 |
return '<div style="color:#067647;font-weight:700;padding:8px 0">%s</div>' % empty
|
|
|
|
| 353 |
+ _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>'
|
| 354 |
+ _h2("📧 " + _L("이메일 위조 방지", "Email anti-spoofing", lang)) + '<div style="display:flex;gap:10px">' + _pill(es.get("spf"), "SPF") + " " + _pill(es.get("dmarc"), "DMARC") + '</div>'
|
| 355 |
+ incon
|
| 356 |
+
+ _h2("📋 " + _L("전사이트 점검 체크리스트", "Full-site checklist", lang)) + _checklist(dp.get("checks", []), lang)
|
| 357 |
+ _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>'
|
| 358 |
)
|
| 359 |
|
|
|
|
| 380 |
+ '<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))
|
| 381 |
+ '<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))
|
| 382 |
+ '</div>'
|
| 383 |
+
+ _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)
|
| 384 |
+
+ _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)
|
| 385 |
+ _h2("🤖 " + _L("AI 로봇 접근 (AI 가시성)", "AI crawler access (AI visibility)", lang))
|
| 386 |
+ '<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>'
|
| 387 |
+
+ _traffic(d.get('matrix', {}), lang) + _cards(geo, _L("✓ 기계 판독 신호 양호", "✓ Machine-readability signals are healthy", lang), lang) + _checklist(d.get('checks', {}).get('geo', []), lang)
|
| 388 |
+ '<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>'
|
| 389 |
+ deep_html
|
| 390 |
+ (_upsell(lang) if show_upsell else "")
|
| 391 |
+ _h2("✓ " + _L("잘 되어 있는 점", "What's done well", lang)) + '<div>' + good + '</div>'
|
| 392 |
+ _h2("📌 " + _L("조치 우선순위", "Action priorities", lang)) + _priority(allf, lang)
|
| 393 |
+
+ _consult_cta(nhi, nmid, nlow, lang)
|
|
|
|
|
|
|
| 394 |
+ '<div style="color:#9ca3af;font-size:11px;margin-top:14px;border-top:1px solid #e4e9ef;padding-top:10px">' + _L(
|
| 395 |
"본 진단은 비침투·비파괴로 수행되었으며 대상 서비스에 영향을 주지 않았습니다. 결과는 측정 시점(" + today + ") 기준. 서비스: 퀀트ID(QuantID) · 엔진: 치토스(CHITOS).",
|
| 396 |
"This diagnosis was non-intrusive and non-destructive and did not affect the target. Results as of " + today + ". Service: QuantID · Engine: CHITOS.", lang) + '</div>'
|