feat: render deep email/DNS/site-security sections
Browse files
app.py
CHANGED
|
@@ -168,6 +168,67 @@ def _upsell(lang):
|
|
| 168 |
+ '</div></div>')
|
| 169 |
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
def _checklist(items, lang):
|
| 172 |
if not items:
|
| 173 |
return ""
|
|
@@ -351,7 +412,9 @@ def build_report(d, deep=False, verified_email=None, lang="ko", show_upsell=Fals
|
|
| 351 |
_L("메타설명 없음", "No meta desc.", lang), dp.get("no_meta", 0), _L("얇은 페이지", "Thin pages", lang), dp.get("thin", 0),
|
| 352 |
_L("색인차단", "noindex", lang), dp.get("noindex", 0), _L("canonical 없음", "No canonical", lang), dp.get("no_canonical", 0))
|
| 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("이메일
|
|
|
|
|
|
|
| 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>'
|
|
|
|
| 168 |
+ '</div></div>')
|
| 169 |
|
| 170 |
|
| 171 |
+
def _statrow(label, ok, val="", warn=False):
|
| 172 |
+
col = "#16a34a" if ok else ("#b54708" if warn else "#dc2626")
|
| 173 |
+
sym = "✓" if ok else ("!" if warn else "✕")
|
| 174 |
+
v = ('<span style="color:#6b7280;font-size:11.5px;margin-left:6px">%s</span>' % html.escape(str(val))) if val else ""
|
| 175 |
+
return ('<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid #eef1f6;border-radius:8px">'
|
| 176 |
+
'<span style="color:%s;font-weight:900;width:13px;text-align:center;flex:none">%s</span>'
|
| 177 |
+
'<span style="font-size:12.5px;color:#374151;flex:1">%s</span>%s</div>') % (col, sym, html.escape(label), v)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _email_block(ed, es, lang):
|
| 181 |
+
if not ed:
|
| 182 |
+
return ('<div style="display:flex;gap:6px">'
|
| 183 |
+
+ _statrow("SPF", bool(es.get("spf")), warn=not es.get("spf"))
|
| 184 |
+
+ _statrow("DMARC", bool(es.get("dmarc")), warn=not es.get("dmarc")) + '</div>')
|
| 185 |
+
spf_ok = ed.get("spf") and ed.get("spf_qual") == "-all"
|
| 186 |
+
dmarc_ok = ed.get("dmarc") and ed.get("dmarc_p") != "none"
|
| 187 |
+
rows = [
|
| 188 |
+
_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)),
|
| 189 |
+
_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)),
|
| 190 |
+
_statrow(_L("DKIM 서명", "DKIM signing", lang), ed.get("dkim", False), warn=not ed.get("dkim", False)),
|
| 191 |
+
_statrow(_L("DMARC 정책", "DMARC policy", lang), dmarc_ok, "p=" + str(ed.get("dmarc_p", "none")), warn=(ed.get("dmarc") and not dmarc_ok)),
|
| 192 |
+
_statrow(_L("BIMI (브랜드 로고)", "BIMI (brand logo)", lang), ed.get("bimi", False), warn=not ed.get("bimi", False)),
|
| 193 |
+
_statrow(_L("MTA-STS (전송 암호화 강제)", "MTA-STS (enforced mail TLS)", lang), ed.get("mta_sts", False), warn=not ed.get("mta_sts", False)),
|
| 194 |
+
_statrow(_L("TLS-RPT (메일 TLS 리포트)", "TLS-RPT (mail TLS reporting)", lang), ed.get("tls_rpt", False), warn=not ed.get("tls_rpt", False)),
|
| 195 |
+
]
|
| 196 |
+
return '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">' + "".join(rows) + '</div>'
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _dns_block(ed, lang):
|
| 200 |
+
if not ed:
|
| 201 |
+
return "—"
|
| 202 |
+
rows = [
|
| 203 |
+
_statrow("DNSSEC", ed.get("dnssec", False), warn=not ed.get("dnssec", False)),
|
| 204 |
+
_statrow(_L("CAA (인증서 발급 통제)", "CAA (cert issuance control)", lang), ed.get("caa", False), warn=not ed.get("caa", False)),
|
| 205 |
+
_statrow(_L("MX (메일 수신)", "MX (mail receiving)", lang), ed.get("mx", 0) > 0, "%d" % ed.get("mx", 0)),
|
| 206 |
+
_statrow(_L("NS 이중화 (2+)", "NS redundancy (2+)", lang), ed.get("ns", 0) >= 2, "%d" % ed.get("ns", 0), warn=(ed.get("ns", 0) < 2)),
|
| 207 |
+
]
|
| 208 |
+
return '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">' + "".join(rows) + '</div>'
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def _sitesec_block(ss, extra_exposed, lang):
|
| 212 |
+
if not ss:
|
| 213 |
+
return "—"
|
| 214 |
+
n = ss.get("pages", 0) or 1
|
| 215 |
+
sri_t = ss.get("sri_total", 0)
|
| 216 |
+
rows = [
|
| 217 |
+
_statrow(_L("전 페이지 HTTPS", "HTTPS on all pages", lang), ss.get("https_pages", 0) == n, "%d/%d" % (ss.get("https_pages", 0), n)),
|
| 218 |
+
_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),
|
| 219 |
+
_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 ""),
|
| 220 |
+
_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)),
|
| 221 |
+
_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 ""),
|
| 222 |
+
_statrow(_L("추가 민감 파일 비노출", "No extra sensitive files", lang), not extra_exposed, (", ".join(extra_exposed) if extra_exposed else "")),
|
| 223 |
+
]
|
| 224 |
+
ext = ss.get("ext_scripts", [])
|
| 225 |
+
extd = ""
|
| 226 |
+
if ext:
|
| 227 |
+
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)
|
| 228 |
+
extd = '<div style="font-size:11.5px;color:#6b7280;margin-top:6px">' + _L("외부 스크립트 출처: ", "Third-party script sources: ", lang) + '</div>' + chips
|
| 229 |
+
return '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">' + "".join(rows) + '</div>' + extd
|
| 230 |
+
|
| 231 |
+
|
| 232 |
def _checklist(items, lang):
|
| 233 |
if not items:
|
| 234 |
return ""
|
|
|
|
| 412 |
_L("메타설명 없음", "No meta desc.", lang), dp.get("no_meta", 0), _L("얇은 페이지", "Thin pages", lang), dp.get("thin", 0),
|
| 413 |
_L("색인차단", "noindex", lang), dp.get("noindex", 0), _L("canonical 없음", "No canonical", lang), dp.get("no_canonical", 0))
|
| 414 |
+ _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>'
|
| 415 |
+
+ _h2("📧 " + _L("이메일 보안 심화 (SPF·DKIM·DMARC·BIMI)", "Email security — deep (SPF·DKIM·DMARC·BIMI)", lang)) + _email_block(dp.get("email_dns", {}), es, lang)
|
| 416 |
+
+ _h2("🌐 " + _L("DNS·도메인 심화", "DNS & domain — deep", lang)) + _dns_block(dp.get("email_dns", {}), lang)
|
| 417 |
+
+ _h2("🧱 " + _L("전사이트 보안 일관성", "Site-wide security consistency", lang)) + _sitesec_block(dp.get("site_sec", {}), dp.get("extra_exposed", []), lang)
|
| 418 |
+ incon
|
| 419 |
+ _h2("📋 " + _L("전사이트 점검 체크리스트", "Full-site checklist", lang)) + _checklist(dp.get("checks", []), lang)
|
| 420 |
+ _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>'
|