From 1acab0917b34276b9d493598d4e7b6d330b465ad Mon Sep 17 00:00:00 2001 From: nercone-dev Date: Sun, 19 Apr 2026 18:54:47 +0900 Subject: [PATCH] -- --- public/tools.html | 2 +- .../tools/tls-test/assets/tls-test-results.js | 124 ++++ public/tools/tls-test/assets/tls-test.css | 551 +++++++++++++++--- public/tools/tls-test/assets/tls-test.js | 91 ++- public/tools/tls-test/index.html | 74 ++- public/tools/tls-test/results.html | 481 ++++++++++++--- public/tools/tls-test/status.html | 45 +- pyproject.toml | 2 +- src/nercone_website/server.py | 109 +++- src/nercone_website/tools/tls_test/db.py | 59 +- src/nercone_website/tools/tls_test/engine.py | 429 ++++++++------ .../tools/tls_test/handshake_sim.py | 79 +-- src/nercone_website/tools/tls_test/hsts.py | 6 +- src/nercone_website/tools/tls_test/http.py | 9 + src/nercone_website/tools/tls_test/http3.py | 7 +- .../tools/tls_test/preload/check.py | 47 +- .../tools/tls_test/protocol/constants.py | 7 + .../tools/tls_test/protocol/kex.py | 71 ++- .../tools/tls_test/protocol/probes.py | 176 +++++- src/nercone_website/tools/tls_test/schemas.py | 24 +- .../tools/tls_test/vulns/renego.py | 169 ++++-- 21 files changed, 1976 insertions(+), 586 deletions(-) create mode 100644 public/tools/tls-test/assets/tls-test-results.js diff --git a/public/tools.html b/public/tools.html index c538c46..30e3701 100644 --- a/public/tools.html +++ b/public/tools.html @@ -7,7 +7,7 @@

Nercone TLS Test

-

仕様書っぽいやつ以外ほぼ全部Claudeに作らせたバグ多めで評価厳しめのTLSサーバーテストツール

+

任意のホストに対して TLS/SSL 設定の詳細チェックとランク付けを行うWebサービス

tls-test
diff --git a/public/tools/tls-test/assets/tls-test-results.js b/public/tools/tls-test/assets/tls-test-results.js new file mode 100644 index 0000000..91f021f --- /dev/null +++ b/public/tools/tls-test/assets/tls-test-results.js @@ -0,0 +1,124 @@ +(function () { + // ---- Tab switching ---- + const tabs = document.querySelectorAll(".tls-tab"); + const panels = document.querySelectorAll(".tls-tab-panel"); + + function activate(name) { + tabs.forEach((btn) => { + const active = btn.dataset.tab === name; + btn.classList.toggle("is-active", active); + btn.setAttribute("aria-selected", active ? "true" : "false"); + }); + panels.forEach((panel) => { + panel.classList.toggle("is-active", panel.dataset.panel === name); + }); + try { + if (history.replaceState) { + history.replaceState(null, "", "#" + name); + } + } catch (_) {} + } + + if (tabs.length && panels.length) { + tabs.forEach((btn) => { + btn.addEventListener("click", () => activate(btn.dataset.tab)); + }); + const hash = (location.hash || "").replace(/^#/, ""); + const valid = ["summary", "reliability", "safety", "vulnerabilities", "compatibility", "log", "json"]; + if (valid.indexOf(hash) !== -1) { + activate(hash); + } + } + + // ---- JSON-copy button (inside the JSON tab) ---- + const copyJsonBtn = document.getElementById("tls-copy-json"); + const rawJsonEl = document.getElementById("tls-raw-json"); + if (copyJsonBtn && rawJsonEl) { + const origLabel = copyJsonBtn.textContent; + copyJsonBtn.addEventListener("click", async () => { + const text = rawJsonEl.textContent || ""; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + } else { + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (_) {} + document.body.removeChild(ta); + } + copyJsonBtn.textContent = "コピーしました"; + copyJsonBtn.classList.add("is-done"); + setTimeout(() => { + copyJsonBtn.textContent = origLabel; + copyJsonBtn.classList.remove("is-done"); + }, 1500); + } catch (_) { + copyJsonBtn.textContent = "コピー失敗"; + setTimeout(() => { copyJsonBtn.textContent = origLabel; }, 1500); + } + }); + } + + // ---- PDF / print button ---- + const printBtn = document.getElementById("tls-print-pdf"); + if (printBtn) { + printBtn.addEventListener("click", () => { + // Expand all tab panels so the printed PDF contains every finding, + // then restore the previous state after the print dialog closes. + const prev = []; + panels.forEach((panel) => { + prev.push([panel, panel.classList.contains("is-active")]); + panel.classList.add("is-active"); + }); + const restore = () => { + prev.forEach(([panel, wasActive]) => { + panel.classList.toggle("is-active", wasActive); + }); + window.removeEventListener("afterprint", restore); + }; + window.addEventListener("afterprint", restore); + try { + window.print(); + } catch (_) { + restore(); + } + }); + } + + // ---- Copy-link button ---- + const copyBtn = document.getElementById("tls-copy-link"); + if (copyBtn) { + const origLabel = copyBtn.textContent; + copyBtn.addEventListener("click", async () => { + const link = copyBtn.dataset.link || location.href; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(link); + } else { + // Fallback: use a hidden textarea + document.execCommand + const ta = document.createElement("textarea"); + ta.value = link; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (_) {} + document.body.removeChild(ta); + } + copyBtn.textContent = "コピーしました"; + copyBtn.classList.add("is-done"); + setTimeout(() => { + copyBtn.textContent = origLabel; + copyBtn.classList.remove("is-done"); + }, 1500); + } catch (_) { + copyBtn.textContent = "コピー失敗"; + setTimeout(() => { copyBtn.textContent = origLabel; }, 1500); + } + }); + } +})(); diff --git a/public/tools/tls-test/assets/tls-test.css b/public/tools/tls-test/assets/tls-test.css index 62cd77e..9c87dc7 100644 --- a/public/tools/tls-test/assets/tls-test.css +++ b/public/tools/tls-test/assets/tls-test.css @@ -1,124 +1,495 @@ -.tls-form { - display: flex; - flex-direction: column; - gap: 8px; - margin: 12px 0; -} -.tls-input { - background-color: #202020; - color: #E0E0E0; - border: 1px solid #3a3a3a; - border-radius: 6px; - padding: 10px 12px; - font-family: inherit; - font-size: inherit; -} -.tls-input:focus { - outline: none; - border-color: #00C0FF; -} -.tls-submit { - align-self: flex-start; - background-color: #202020; - color: #E0E0E0; - border: 1px solid #3a3a3a; - border-radius: 6px; - padding: 10px 20px; - font-family: inherit; -} -.tls-submit:hover { - border-color: #00C878; - color: #00C878; -} +/* ===================================================== + * Nercone TLS Test — shared styles + * Palette comes from /public/assets/css/style.css + * #1A1A1A = page bg #272727 = block bg + * #202020 = input bg #3a3a3a = border + * #E0E0E0 = tx #939393 = tx-alt + * #00C878 = bright-green #00C0FF = bright-blue + * ===================================================== */ -.tls-progress-track { - height: 6px; - background-color: #202020; - border-radius: 3px; - overflow: hidden; - margin: 12px 0; -} -.tls-progress-bar { - height: 100%; - background-color: #00C878; - transition: width 0.3s ease; -} - -.tls-log { - max-height: 400px; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 4px; - font-family: "MesloLGS NF", "Menlo", "Consolas", monospace; - font-size: 10pt; -} -.tls-log-row { - padding: 2px 0; -} - -.tls-summary { - display: flex; - align-items: center; - gap: 24px; - flex-wrap: wrap; -} -.tls-rank-badge { - width: 120px; - height: 120px; - border-radius: 50%; - background-color: #202020; +/* ------ Landing page ------ */ +.tls-landing { + min-height: calc(100vh - 220px); display: flex; flex-direction: column; align-items: center; justify-content: center; - border: 4px solid currentColor; - flex-shrink: 0; + padding: 40px 24px; + text-align: center; + gap: 0; } +.tls-landing-title { + margin: 0 0 8px 0; + font-size: clamp(40pt, 6vw, 64pt); + line-height: 1.1; + color: #FFFFFF; + letter-spacing: -0.01em; +} +.tls-landing-subtitle { + margin: 0 0 40px 0; + font-size: 13pt; +} +.tls-landing-form { + display: flex; + align-items: stretch; + gap: 10px; + width: min(560px, 100%); + margin: 0 auto; +} +.tls-landing-input { + flex: 1; + min-width: 0; + background-color: #3a3a3a; + color: #E0E0E0; + border: 1px solid #4a4a4a; + border-radius: 6px; + padding: 12px 16px; + font-family: inherit; + font-size: 13pt; +} +.tls-landing-input::placeholder { color: #939393; } +.tls-landing-input:focus { + outline: none; + border-color: #00C0FF; +} +.tls-landing-submit { + background-color: #0096D0; + color: #FFFFFF; + border: none; + border-radius: 6px; + padding: 12px 28px; + font-family: inherit; + font-size: 13pt; + transition: background-color 0.15s ease; +} +.tls-landing-submit:hover { background-color: #00B4F0; } +.tls-landing-submit:active { background-color: #0080B8; } +.tls-landing-error { + margin-top: 12px; +} +.tls-landing-links { + margin-top: 180px; + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; +} +.tls-landing-links a { + color: #939393; + text-decoration: underline; +} +.tls-landing-links a:hover { color: #E0E0E0; } + +.tls-aux-section { + max-width: 900px; + margin: 0 auto; + scroll-margin-top: 80px; +} +.tls-aux-section ul { padding-left: 20px; } +.tls-aux-section li { margin: 4px 0; line-height: 1.6; } + +/* ------ Status page ------ */ +.tls-status { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 16px 40px; + text-align: center; +} +.tls-status-subtitle { + margin: 4px 0 28px 0; + font-size: 14pt; +} +.tls-status-progress { + width: min(800px, 100%); + margin: 0 auto 28px auto; +} +.tls-status-phase { + margin: 0 0 8px 0; +} +.tls-status-testid { + margin: 10px 0 0 0; +} +.tls-status-testid code { + font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; +} +.tls-progress-track { + width: 100%; + height: 6px; + background-color: #2a2a2a; + border-radius: 3px; + overflow: hidden; +} +.tls-progress-bar { + height: 100%; + background-color: #00C878; + transition: width 0.25s ease; +} + +/* Status-page log block wrapper (the lighter-gray card in Image 2) */ +.tls-log-wrap { + width: min(1100px, 100%); + margin: 0 auto; + background-color: #272727; + border-radius: 8px; + padding: 18px 22px; + text-align: left; +} + +/* ------ Log (shared between status + results) ------ */ +.tls-log { + font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; + font-size: 10.5pt; + line-height: 1.55; + max-height: 60vh; + overflow-y: auto; +} +.tls-log-row { + display: grid; + grid-template-columns: 13em minmax(28em, auto) 1fr; + column-gap: 16px; + padding: 1px 0; + word-break: break-word; + align-items: baseline; +} +.tls-log-cat { + white-space: nowrap; +} +.tls-log-msg { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.tls-log-msg > .font-bold { margin-right: 6px; } +.tls-log-detail { + min-width: 0; +} +/* narrower viewports: collapse to 2 rows per entry */ +@media (max-width: 900px) { + .tls-log-row { + grid-template-columns: 1fr; + padding: 3px 0; + } + .tls-log-msg { white-space: normal; } +} + +/* ------ Results page ------ */ + +/* The banner header that replaces the site header on results pages. + * The global `header { position: fixed }` rule in /assets/css/style.css is + * overridden here because our banner flows inline — unlike the site header + * which floats over everything. We also neutralise the 60px top-padding + * that
carries to clear the (now absent) floating site header. */ +.tls-results-header { + position: static; + display: block; + background-color: #272727; + padding: 28px clamp(16px, 4vw, 40px) 0; + margin-bottom: 0; + border-bottom: 1px solid #1a1a1a; + backdrop-filter: none; +} +.tls-results-header::before { + /* the global header::before adds a blurred backdrop — suppress it */ + display: none !important; +} +.tls-results-header + main { + padding-top: 24px; +} +.tls-results-head { + display: flex; + align-items: center; + gap: 20px; + flex-wrap: wrap; +} +.tls-rank-badge { + width: 110px; + height: 110px; + border-radius: 50%; + background-color: currentColor; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.05); +} +.tls-rank-badge > * { color: #0a0a0a; } .tls-rank-letters { - font-size: 48pt; + font-size: 36pt; font-weight: 700; line-height: 1; + letter-spacing: -0.02em; } .tls-rank-score { font-size: 10pt; - color: #939393; - margin-top: 4px; + margin-top: 2px; + opacity: 0.75; } -.tls-summary-meta { - min-width: 200px; +.tls-results-meta { + min-width: 220px; } -.tls-target { - margin: 0 0 8px 0; +.tls-results-target { + margin: 0 0 4px 0; + font-size: 22pt; + font-weight: 700; + color: #FFFFFF; word-break: break-all; } +.tls-results-metaline { + margin: 0 0 2px 0; + font-size: 10.5pt; + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.tls-results-testid { + margin: 4px 0 0 0; + font-size: 10pt; +} +.tls-results-testid code { + font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; + padding: 0; +} +.tls-results-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} +.tls-btn { + display: inline-block; + padding: 12px 22px; + border-radius: 6px; + border: none; + font-family: inherit; + font-size: 11pt; + text-decoration: none; + transition: background-color 0.15s ease, color 0.15s ease; + line-height: 1.2; +} +.tls-btn-secondary { + background-color: #3a3a3a; + color: #E0E0E0; +} +.tls-btn-secondary:hover { background-color: #4a4a4a; } +.tls-btn-primary { + background-color: #0096D0; + color: #FFFFFF; +} +.tls-btn-primary:hover { background-color: #00B4F0; } +.tls-btn-primary.is-done { background-color: #00A050; } -.tls-finding { - margin: 4px 0; - line-height: 1.5; +/* Tabs — sit at the bottom of the results header banner. */ +.tls-results-header .tls-tabs { + margin: 24px -1px 0 -1px; + padding: 0; +} +.tls-tabs { + display: flex; + flex-wrap: wrap; + gap: 0; + margin: 12px 0 16px 0; + border-bottom: 1px solid #1a1a1a; +} +.tls-tab { + background: transparent; + color: #939393; + border: none; + border-bottom: 2px solid transparent; + padding: 10px 18px 10px 18px; + font-family: inherit; + font-size: 12pt; + transition: color 0.15s ease, border-color 0.15s ease; + margin-bottom: -1px; +} +.tls-tab:hover { + color: #E0E0E0; +} +.tls-tab.is-active { + color: #FFFFFF; + font-weight: 600; + border-bottom-color: #E0E0E0; +} +.tls-tab-panels { + padding: 0 clamp(16px, 4vw, 40px) 40px; +} +.tls-tab-panel { + display: none; +} +.tls-tab-panel.is-active { + display: block; } +/* Structured sections inside each tab panel. */ +.tls-section { + margin-top: 24px; +} +.tls-section:first-child { + margin-top: 4px; +} +.tls-section-title { + font-size: 14pt; + font-weight: 600; + color: #FFFFFF; + margin: 0 0 10px 0; + padding-bottom: 6px; + border-bottom: 1px solid #3a3a3a; +} + +/* Standard result tables. */ .tls-table { width: 100%; border-collapse: collapse; - margin-top: 8px; + font-size: 11pt; + background-color: #202020; + border-radius: 6px; + overflow: hidden; + color: #E0E0E0; } -.tls-table th, .tls-table td { - border-bottom: 1px solid #3a3a3a; - padding: 6px 8px; +.tls-table th, +.tls-table td { + padding: 10px 12px; text-align: left; + vertical-align: top; + border-bottom: 1px solid #2c2c2c; } -.tls-table th { +.tls-table thead th { + background-color: #2a2a2a; color: #939393; - font-weight: 400; - font-size: 10pt; + font-weight: 500; + font-size: 10.5pt; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.tls-table tbody tr:last-child th, +.tls-table tbody tr:last-child td { + border-bottom: none; +} +.tls-table code { + font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; + font-size: 10.5pt; + background-color: transparent; + padding: 0; } +/* Cipher suites per version. */ +.tls-cipher-block { + margin-top: 14px; +} +.tls-cipher-version { + margin: 0 0 6px 0; + font-size: 11.5pt; + color: #E0E0E0; + font-weight: 600; +} +.tls-cipher-list { + list-style: none; + padding: 10px 14px; + margin: 0; + background-color: #202020; + border-radius: 6px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 4px 16px; +} +.tls-cipher-list code { + font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; + font-size: 10pt; + color: #E0E0E0; + background: transparent; + padding: 0; +} + +/* JSON tab. */ +.tls-json-meta { + display: flex; + gap: 10px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + margin-bottom: 10px; +} .tls-raw { - max-height: 400px; + max-height: 640px; overflow: auto; background-color: #202020; - padding: 12px; + padding: 14px 16px; border-radius: 6px; font-size: 10pt; + margin: 0; +} +.tls-raw code { + font-family: "MesloLGS Nerd Font", "MesloLGS NF", "Menlo", "Consolas", monospace; + white-space: pre; + background: transparent; + padding: 0; +} + +/* ------------------------------------------------------------------ + * Print stylesheet — "PDFをダウンロード" uses window.print() which + * lets the user save the page as a PDF from the browser print dialog. + * Hide chrome (tabs, buttons, site header/footer) and expand all tab + * panels so the resulting PDF contains every finding. + * ------------------------------------------------------------------ */ +@media print { + html, body { + background: #ffffff !important; + color: #101010 !important; + } + /* Hide site-wide chrome and interactive controls. */ + footer, .tls-tabs, .tls-results-actions, + #tls-copy-json, script, .tls-landing-links { + display: none !important; + } + /* Keep the results-header banner but make it paper-friendly. */ + .tls-results-header { + background: transparent !important; + border: 0 !important; + padding: 0 0 16px 0 !important; + border-bottom: 1px solid #cccccc !important; + } + /* Expand every tab panel so the full report prints. */ + .tls-tab-panels { + padding: 0 !important; + } + .tls-tab-panel { + display: block !important; + break-inside: avoid; + page-break-inside: avoid; + } + .tls-tab-panel + .tls-tab-panel { + margin-top: 16px; + } + .tls-section-title { + color: #101010 !important; + border-bottom: 1px solid #cccccc !important; + } + .tls-table, .tls-cipher-list, .tls-raw { + background: #ffffff !important; + color: #101010 !important; + } + .tls-table thead th, + .tls-table th, .tls-table td { + border-color: #cccccc !important; + background: transparent !important; + color: #101010 !important; + } + .tls-log-row { + color: #101010 !important; + border-bottom: 1px dotted #cccccc; + padding: 4px 0; + } + /* Rank badge keeps its colour via currentColor. */ + .tls-rank-badge { + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + } + .tls-results-target { + color: #101010 !important; + } + a { + color: #101010 !important; + text-decoration: none !important; + } } diff --git a/public/tools/tls-test/assets/tls-test.js b/public/tools/tls-test/assets/tls-test.js index d31234f..ebf6296 100644 --- a/public/tools/tls-test/assets/tls-test.js +++ b/public/tools/tls-test/assets/tls-test.js @@ -5,14 +5,26 @@ const phaseEl = document.getElementById("tls-phase"); const barEl = document.getElementById("tls-progress-bar"); const logEl = document.getElementById("tls-log"); + const verbEl = document.getElementById("tls-status-verb"); + // Colors for the severity label tag. Order Good→Bad maps to + // bright-{green,blue,yellow,red,purple}. Matches SEVERITY_COLORS in + // schemas.py so the live log and results page render identically. const SEV_COLOR = { good: "bright-green", - normal: "bright-yellow", - notgood: "bright-orange", + normal: "bright-blue", + notgood: "bright-yellow", bad: "bright-red", - serious: "magenta", - info: "tx", + serious: "bright-purple", + info: "tx-alt", + }; + const SEV_LABEL = { + good: "GOOD", + normal: "NORMAL", + notgood: "NOT GOOD", + bad: "BAD", + serious: "SERIOUS", + info: "INFO", }; let reconnectAttempts = 0; @@ -20,25 +32,57 @@ let ws = null; let closedByDone = false; - function appendLog(phase, detail, severity) { + function normalizeStep(s) { + // "handshake_sim" → "handshake-sim" for display consistency with the + // server-rendered results page. + return String(s || "info").replace(/_/g, "-").toLowerCase(); + } + + function appendRow(category, body, detail, severity) { if (!logEl) return; const row = document.createElement("div"); row.className = "tls-log-row"; - const label = document.createElement("span"); - label.className = "text-tx-alt font-small"; - label.textContent = `[${phase}] `; + + const cat = document.createElement("span"); + cat.className = "tls-log-cat text-tx-alt"; + cat.textContent = "[" + normalizeStep(category) + "]"; + row.appendChild(cat); + const msg = document.createElement("span"); - msg.className = `text-${SEV_COLOR[severity] || "tx"}`; - msg.textContent = detail || ""; - row.appendChild(label); + msg.className = "tls-log-msg"; + if (severity && SEV_LABEL[severity] && severity !== "info") { + const sev = document.createElement("span"); + sev.className = "text-" + (SEV_COLOR[severity] || "tx") + " font-bold"; + sev.textContent = SEV_LABEL[severity]; + msg.appendChild(sev); + msg.appendChild(document.createTextNode(" ")); + } + // Title is always in the default text color (white). Severity is + // communicated by the coloured label tag, not by tinting the title. + const msgText = document.createElement("span"); + msgText.className = "text-tx"; + msgText.textContent = body || ""; + msg.appendChild(msgText); row.appendChild(msg); + + const det = document.createElement("span"); + det.className = "tls-log-detail text-tx-alt"; + det.textContent = detail || ""; + row.appendChild(det); + logEl.appendChild(row); logEl.scrollTop = logEl.scrollHeight; } - function setProgress(value, phase) { + function setProgress(value, phaseText) { if (barEl) barEl.style.width = `${Math.max(0, Math.min(1, value)) * 100}%`; - if (phaseEl && phase) phaseEl.textContent = phase; + if (phaseEl && phaseText) phaseEl.textContent = phaseText; + } + + function markDone(rank, score) { + if (verbEl) verbEl.textContent = "完了"; + if (barEl) barEl.style.width = "100%"; + if (phaseEl) phaseEl.textContent = `完了 (ランク ${rank}, スコア ${score})`; } function connect() { @@ -56,7 +100,7 @@ return; } if (msg.type === "history") { - (msg.entries || []).forEach((e) => appendLog(e.phase, e.detail, e.severity)); + (msg.entries || []).forEach((e) => appendRow(e.phase, e.detail, "", e.severity)); if (msg.status === "done") { closedByDone = true; location.replace(init.resultsUrl); @@ -64,7 +108,8 @@ return; } if (msg.type === "progress") { - appendLog(msg.phase, msg.detail, msg.severity); + // Phase-level status: single body text with no detail + appendRow(msg.phase, msg.detail, "", msg.severity); if (typeof msg.progress === "number") { setProgress(msg.progress, msg.detail || msg.phase); } @@ -72,21 +117,25 @@ } if (msg.type === "finding") { const f = msg.finding || {}; - appendLog(f.category || "finding", `${f.severity_label || ""} ${f.title || ""} ${f.detail ? "— " + f.detail : ""}`.trim(), f.severity || "info"); + // Findings: show severity tag + title as the body, and detail in column 3. + // The "[step]" prefix uses the phase slug (e.g. "handshake-sim") + // rather than the coarser group ("vulnerabilities"). + const step = f.step || f.group || f.category || "info"; + appendRow(step, f.title || "", f.detail || "", f.severity || "info"); return; } if (msg.type === "done") { closedByDone = true; - setProgress(1.0, `done (rank ${msg.rank}, score ${msg.score})`); - location.replace(msg.redirect || init.resultsUrl); + markDone(msg.rank, msg.score); + setTimeout(() => location.replace(msg.redirect || init.resultsUrl), 300); return; } if (msg.type === "error") { - appendLog("error", msg.message || "engine failed", "serious"); + appendRow("error", msg.message || "engine failed", "", "serious"); return; } if (msg.type === "started") { - appendLog("started", msg.target || "", "info"); + appendRow("init", msg.target || "", "", "info"); return; } }; @@ -101,7 +150,7 @@ function scheduleReconnect() { if (closedByDone) return; if (reconnectAttempts >= MAX_RECONNECTS) { - appendLog("ws", "WebSocket 接続が切断されました。ページをリロードしてください。", "bad"); + appendRow("ws", "WebSocket 接続が切断されました。ページをリロードしてください。", "", "bad"); return; } const delay = Math.min(10_000, 1000 * Math.pow(2, reconnectAttempts)); diff --git a/public/tools/tls-test/index.html b/public/tools/tls-test/index.html index 7fdffbb..98c22dc 100644 --- a/public/tools/tls-test/index.html +++ b/public/tools/tls-test/index.html @@ -1,41 +1,55 @@ {% extends "/base.html" %} {% block title %}Nercone TLS Test{% endblock %} {% block title_suffix %}TLS Test{% endblock %} -{% block description %}任意のホストに対して TLS/SSL 設定の詳細チェックとランク付けを行います。{% endblock %} +{% block description %}Nercone TLS Testは、任意のホストに対して TLS/SSL 設定の詳細チェックとランク付けを行うWebサービスです。{% endblock %} {% block header_desc %}ただのTLS/SSL設定分析サービス{% endblock %} {% block extra_head %} {% endblock %} {% block content %} -
-

TLS Test

-

任意のホストに対してTLS/SSL設定の詳細チェックを行い、SSS〜Rの21段階でランク付けします。

-
- - +

Nercone TLS Test

+

ただのTLS/SSL設定分析サービス

+ + - - {% if error %} -

{{ error }}

- {% endif %} + placeholder="nercone.dev" class="tls-landing-input" + aria-label="対象ホスト"> +
-

プロトコルバージョン・暗号スイート・証明書・HSTS・CAA・HTTP/1-2-3 対応・主要脆弱性・ハンドシェイクシミュレーションを検査します。

-

IP 直接入力の場合、SNI/証明書名の一致を判定できないため減点対象となることがあります。

-
-
-

利用上の注意

-
    -
  • このツールはとりあえず動けば良いやと、Claudeの性能チェックも兼ねて、仕様書っぽいやつ以外ほぼ丸ごと作らせたため、バグがいくつかあると思います。レイアウトがあまり良くない箇所とかもすでに何箇所か発見しています。今後改善予定です。
  • -
  • このツールは指定されたホストに対して実際に TLS 接続を行います。第三者のサーバーに対するスキャンは、対象サーバーの利用規約や法律を遵守した上で行ってください。
  • -
  • レート制限: 同一 IP から同時実行は 1 件、1 時間あたり 10 件までです。
  • -
  • 結果は7日間保持されます。テストIDを知っている人は同じ結果を閲覧できます。
  • -
  • Nercone TLS Testサービスが使用するUser-Agent文字列はnercone-tls-test/1.0です。
  • -
-
-
-

API

-

同等の機能を JSON API として提供しています。

-
POST /api/tools/tls-test/scan
Content-Type: application/json
{"target": "example.com"}
-
+ {% if error %} +

{{ error }}

+ {% endif %} + + + +
+
+

使用上の注意

+
    +
  • このツールは指定されたホストに対して実際に TLS 接続を行います。第三者のサーバーに対するスキャンは、対象サーバーの利用規約や法律を遵守した上で行ってください。
  • +
  • レート制限: 同一 IP から同時実行は 1 件、1 時間あたり 10 件までです。
  • +
  • 結果は最長 1 年間保持されます。少なくとも 7 日間は保存され、7 日経過後は件数が多い場合にのみ古い順に自動削除されます。テストIDを知っている人は同じ結果を閲覧できます。
  • +
  • ドメイン名 / サブドメイン / IPv4 / IPv6 / host:port 形式を受け付けます。IP 直接入力の場合、SNI/証明書名の一致を判定できないため減点対象となることがあります。
  • +
  • Nercone TLS Test が使用する User-Agent 文字列は nercone-tls-test/1.0 です。
  • +
+
+
+ +
+
+

APIドキュメント

+

同等の機能を JSON API として提供しています。

+
POST /api/tools/tls-test/scan
+Content-Type: application/json
+
+{"target": "example.com"}
+

以下のエンドポイントで進捗・結果を取得できます。

+
GET /api/tools/tls-test/status/{test_id}
+GET /api/tools/tls-test/results/{test_id}
+
+
{% endblock %} diff --git a/public/tools/tls-test/results.html b/public/tools/tls-test/results.html index 20dae86..ea92926 100644 --- a/public/tools/tls-test/results.html +++ b/public/tools/tls-test/results.html @@ -1,12 +1,17 @@ {% extends "/base.html" %} {% block title %}{{ result.target }} (ランク{{ result.rank }}) - Nercone TLS Test{% endblock %} {% block title_suffix %}TLS Test{% endblock %} -{% block description %}TLS Test の結果ページです。対象: {{ result.target }} / ランク: {{ result.rank }}。{% endblock %} -{% block header_desc %}Results{% endblock %} +{% block description %}Nercone TLS Test の結果ページです。対象: {{ result.target }} / スコア: {{ "%.1f"|format(result.score or 0) }} / ランク: {{ result.rank }}。{% endblock %} +{% block header_desc %}ただのTLS/SSL設定分析サービス{% endblock %} {% block extra_head %} {% endblock %} -{% block content %} + +{# ------------------------------------------------------------------ + Replace the site header with a results banner that carries the rank + badge, target, metadata, action buttons, and the tab bar at its foot. + ------------------------------------------------------------------ #} +{% block custom_header %} {% set rank_color_map = { 'SSS': 'bright-green', 'SS': 'bright-green', 'S': 'bright-green', 'A': 'green', 'B': 'green', 'C': 'green', @@ -15,86 +20,396 @@ 'J': 'bright-orange', 'K': 'bright-orange', 'L': 'bright-orange', 'M': 'orange', 'N': 'orange', 'O': 'bright-red', 'P': 'bright-red', - 'Q': 'red', 'R': 'purple' + 'Q': 'red', 'R': 'bright-purple' } %} - {% set rank_color = rank_color_map.get(result.rank or job.rank, 'tx') %} -
-
- {{ result.rank or job.rank or '?' }} - {{ "%.2f"|format(result.score or job.score or 0) }} + {% set rank = result.rank or job.rank or '?' %} + {% set rank_color = rank_color_map.get(rank, 'tx') %} + +
+
+
+ {{ rank }} + {{ "%.0f"|format(result.score or job.score or 0) }} +
+
+

{{ result.target or job.target }}

+

+ 実行日時 + {{ finished_at_display or job.finished_at or "—" }} + 所要時間 + {{ "%.0f"|format((result.duration or 0) * 1000) }}ms +

+

{{ test_id }}

+ {% if result.error %} +

{{ result.error }}

+ {% endif %} +
+
+ + +
-
-

{{ result.target or job.target }}

-

host={{ result.host }} port={{ result.port }}

-

実施: {{ job.started_at | default("") }} / 所要時間: {{ "%.2f"|format(result.duration or 0) }}秒 / テストID: {{ test_id }}

-

結果は7日間保存され、その後自動的に削除されます。

- {% if result.error %} -

{{ result.error }}

- {% endif %} -
-
- - {% set category_titles = { - 'protocol': 'SSL/TLS Versions', - 'cipher': 'Cipher Suites', - 'kex': 'Key Exchange', - 'cert': 'Certificate', - 'trust': 'Trust Stores', - 'hsts': 'HSTS / Preload', - 'caa': 'CAA', - 'http': 'HTTP', - 'vuln': 'Vulnerabilities', - 'compat': 'Client Compatibility', - 'connectivity': 'Connectivity', - 'engine': 'Engine' - } %} - {% for cat_key, findings in categories.items() %} -
-

{{ category_titles.get(cat_key, cat_key|capitalize) }}

- {% for f in findings %} -

- [{{ f.severity_label }}] - {{ f.title }} - {% if f.detail %}— {{ f.detail }}{% endif %} -

- {% endfor %} -
- {% endfor %} - - {% if result.data and result.data.handshake_simulation %} -
-

Handshake Simulation

- - - - - - {% for s in result.data.handshake_simulation %} - - - - - - {% endfor %} - -
ClientVersionCipher
{{ s.client }} - {% if s.connected %} - {{ s.negotiated_version }} - {% else %} - failed - {% endif %} - - {% if s.connected %}{{ s.negotiated_cipher }}{% else %}{{ s.error }}{% endif %} -
-
- {% endif %} - -
-

Raw JSON

-

API でも同じデータを取得できます: GET /api/tools/tls-test/results/{{ test_id }}

-
- Show raw result JSON -
{{ result | tojson(indent=2) }}
-
-
+ + +{% endblock %} + +{% block content %} + {# + Log row for a finding. Columns: + [step] severity-label + title(white) detail(gray) + #} + {% macro finding_row(f) -%} +
+ [{{ (f.step or f.group or f.category or 'info')|replace('_','-')|lower }}] + + {{ f.severity_label|upper }} + {{ f.title }} + + {{ f.detail or '' }} +
+ {%- endmacro %} + + {# A boolean row rendered with green/red text. #} + {% macro yn(value, ok='対応', bad='未対応') -%} + {% if value %}{{ ok }}{% else %}{{ bad }}{% endif %} + {%- endmacro %} + + {# Revocation cell (OCSP / CRL). Translates the technical error + strings from certs/revocation.py into a single clean Japanese + label, so we never print awkward combinations like + 「未確認 no OCSP URL」. #} + {% macro rev_cell(r, kind) -%} + {%- if not r -%} + + {%- elif r.checked and not r.revoked -%} + Not Revoked + {%- if r.source %} ({{ r.source }}){% endif -%} + {%- elif r.checked and r.revoked -%} + Revoked + {%- if r.reason %} ({{ r.reason }}){% endif -%} + {%- else -%} + {%- set err = (r.error or '')|string -%} + {%- if 'no OCSP URL' in err or 'no AIA' in err -%} + {{ kind }} URL が提供されていません + {%- elif 'no CRL DP' in err or 'no http CRL URL' in err -%} + {{ kind }} URL が提供されていません + {%- elif 'all CRLs unreachable' in err -%} + {{ kind }} レスポンダに到達できません + {%- elif err -%} + 確認失敗 + ({{ err }}) + {%- else -%} + 未確認 + {%- endif -%} + {%- endif -%} + {%- endmacro %} + +
+ + {# -------- 概要 -------- #} +
+
+ {% if summary %} + {% for f in summary %}{{ finding_row(f) }}{% endfor %} + {% else %} +
[info]該当項目なし
+ {% endif %} +
+
+ + {# -------- 信頼性 -------- #} +
+ {# Certificate chain table #} + {% set chain = result.data.certificate_chain if result.data else [] %} + {% if chain %} +
+

証明書チェーン

+ + + + + + {% for c in chain %} + + + + + + + + + {% endfor %} + +
#CN / Subject発行者有効期限署名
{{ loop.index }} +
{{ c.common_name or c.subject or '—' }}
+ {% if c.sans %}
SAN: {{ (c.sans or [])|join(', ') }}
{% endif %} +
{{ c.issuer or '—' }} +
{{ c.not_after or '—' }}
+ {% if c.is_expired %}期限切れ + {% elif c.days_until_expiry is defined and c.days_until_expiry is not none %}残 {{ c.days_until_expiry }} 日{% endif %} +
{{ (c.signature_hash_algorithm or '—')|upper }} + {{ c.public_key_algorithm or '—' }}{% if c.public_key_size_bits %} {{ c.public_key_size_bits }}-bit{% endif %} + {% if c.public_key_curve %} ({{ c.public_key_curve }}){% endif %} +
+
+ {% endif %} + + {# Trust stores #} + {% set trust = result.data.trust if result.data else [] %} + {% if trust %} +
+

プラットフォームごとの信頼状況

+ + + + {% for t in trust %} + + + + + + {% endfor %} + +
プラットフォーム信頼状況
{{ t.platform }}{{ yn(t.trusted, '信頼', '未信頼') }}{{ t.error or '—' }}
+
+ {% endif %} + + {# Revocation + HSTS + CAA summary #} + {% set ocsp = (result.data or {}).get('ocsp') %} + {% set crl = (result.data or {}).get('crl') %} + {% set hsts = (result.data or {}).get('hsts') %} + {% set caa = (result.data or {}).get('caa') %} + {% set preload = (result.data or {}).get('preload') %} +
+

失効・HSTS・CAA

+ + + + + + + + +
OCSP{{ rev_cell(ocsp, 'OCSP') }}
CRL{{ rev_cell(crl, 'CRL') }}
HSTS + {%- if not hsts or hsts.error -%} + + {%- elif hsts.present -%} + 有効 + max-age={{ hsts.max_age }}{% if hsts.include_subdomains %}; includeSubDomains{% endif %}{% if hsts.preload %}; preload{% endif %} + {%- else -%} + 無効 + {%- endif -%} +
HSTS preload + {%- if preload -%} + {%- for p in preload -%} + {{ p.browser }}: {{ yn(p.listed, 'Listed', 'Not listed') }}{% if not loop.last %} · {% endif %} + {%- endfor -%} + {%- else -%} + + {%- endif -%} +
CAA + {%- if not caa -%} + + {%- elif caa.records -%} + {{ caa.records|length }} 件 + ({{ caa.effective_host }}) +
{{ caa.records|join('; ') }}
+ {%- else -%} + 未設定 + {%- endif -%} +
+
+ +
+

信頼性に関するすべてのログ

+
+ {% set findings = groups['reliability'] %} + {% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %} + {% else %}
[info]項目なし
{% endif %} +
+
+
+ + {# -------- 安全性 -------- #} +
+ {# Protocol versions #} + {% set versions = (result.data or {}).get('versions') %} + {% if versions %} +
+

対応プロトコル

+ + + + {% for name, ok in versions.items() %} + + {% endfor %} + +
バージョン対応
{{ name }}{{ yn(ok, '有効', '無効') }}
+
+ {% endif %} + + {# Accepted ciphers per version #} + {% set ciphers = (result.data or {}).get('ciphers') %} + {% if ciphers %} +
+

受理された暗号スイート

+ {% for name, cs in ciphers.items() %} + {% if cs %} +
+

{{ name }} ({{ cs|length }})

+
    + {% for c in cs %} +
  • {{ c }}
  • + {% endfor %} +
+
+ {% endif %} + {% endfor %} +
+ {% endif %} + + {# Named groups #} + {% set groups_list = (result.data or {}).get('named_groups') %} + {% if groups_list %} +
+

TLS 1.3 鍵交換グループ

+

{% for g in groups_list %}{{ g }}{% if not loop.last %}, {% endif %}{% endfor %}

+
+ {% endif %} + +
+

安全性に関するすべてのログ

+
+ {% set findings = groups['safety'] %} + {% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %} + {% else %}
[info]項目なし
{% endif %} +
+
+
+ + {# -------- 脆弱性 -------- #} +
+ {% set hb = (result.data or {}).get('heartbleed') %} + {% set ccs = (result.data or {}).get('ccs_injection') %} + {% set renego = (result.data or {}).get('secure_renegotiation') %} + {% set scsv = (result.data or {}).get('fallback_scsv') %} +
+

既知脆弱性の判定

+ + + + + + + + +
脆弱性判定詳細
Heartbleed{% if hb %}{{ yn(not hb.vulnerable, '影響なし', '影響あり') }}{% else %}{% endif %}{% if hb %}{% if hb.heartbeat_extension %}Heartbeat advertised{% endif %}{% if hb.error %} {{ hb.error }}{% endif %}{% endif %}
CCS Injection{% if ccs %}{{ yn(not ccs.vulnerable, '影響なし', '影響あり') }}{% else %}{% endif %}{{ (ccs or {}).get('detail','') }}
Secure Renegotiation{% if renego %}{{ yn(renego.supported, '対応', '未対応') }}{% else %}{% endif %}{{ (renego or {}).get('detail','') }}
TLS_FALLBACK_SCSV{% if scsv %}{{ yn(scsv.supported, '対応', '未対応') }}{% else %}{% endif %}{{ (scsv or {}).get('detail','') }}
+
+ +
+

脆弱性に関するすべてのログ

+
+ {% set findings = groups['vulnerabilities'] %} + {% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %} + {% else %}
[info]項目なし
{% endif %} +
+
+
+ + {# -------- 互換性 -------- #} +
+ {% set http = (result.data or {}).get('http') %} + {% set http3 = (result.data or {}).get('http3') %} + {% set alpn = (result.data or {}).get('alpn') %} +
+

HTTP プロトコル対応

+ + + + + + + {% if http and http.server %}{% endif %} + {% if http and http.alt_svc %}{% endif %} + +
HTTP/1.1{% if http %}{{ yn(http.http1, '対応', '未対応') }}{% else %}{% endif %}
HTTP/2{% if http %}{{ yn(http.http2, '対応', '未対応') }}{% else %}{% endif %}
HTTP/3 (QUIC){% if http3 %}{{ yn(http3.supported, '対応', '未対応') }}{% if http3.error %} {{ http3.error }}{% endif %}{% else %}{% endif %}
ALPN{% if alpn %}{{ alpn }}{% else %}{% endif %}
Server{{ http.server }}
Alt-Svc{{ http.alt_svc }}
+
+ + {% set sim = (result.data or {}).get('handshake_simulation') %} + {% if sim %} +
+

ハンドシェイクシミュレーション

+ + + + {% for s in sim %} + + + + + + + + {% endfor %} + +
クライアント結果プロトコル暗号備考
{{ s.client }}{{ yn(s.connected, 'OK', 'Fail') }}{{ s.negotiated_version or '—' }}{{ s.negotiated_cipher or '—' }}{{ s.error or '' }}
+
+ {% endif %} + +
+

互換性に関するすべてのログ

+
+ {% set findings = groups['compatibility'] %} + {% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %} + {% else %}
[info]項目なし
{% endif %} +
+
+
+ + {# -------- ログ -------- + Replays the live processing log: interleaved progress rows + (from test_progress table) and findings (grouped by step). #} +
+
+
+ {% if log_entries %} + {% for e in log_entries %} + {% if e.kind == 'phase' %} +
+ [{{ (e.phase or 'info')|replace('_','-')|lower }}] + {{ e.detail or '' }} + +
+ {% else %} + {{ finding_row(e.finding) }} + {% endif %} + {% endfor %} + {% else %} +
[info]ログが記録されていません
+ {% endif %} +
+
+
+ + {# -------- JSON -------- #} +
+
+
+ GET /api/tools/tls-test/results/{{ test_id }} + +
+
{{ result | tojson(indent=2) }}
+
+
+
+ + {% endblock %} diff --git a/public/tools/tls-test/status.html b/public/tools/tls-test/status.html index a7f0db8..1d13c40 100644 --- a/public/tools/tls-test/status.html +++ b/public/tools/tls-test/status.html @@ -1,31 +1,36 @@ {% extends "/base.html" %} -{% block title %}Scanning {{ target }} - Nercone TLS Test{% endblock %} +{% block title %}{{ target }} をテスト中 - Nercone TLS Test{% endblock %} {% block title_suffix %}TLS Test{% endblock %} -{% block description %}スキャンの進捗を表示しています。{% endblock %} -{% block header_desc %}スキャン中です...{% endblock %} +{% block description %}{{ target }} に対する TLS Test の進捗を表示しています。{% endblock %} +{% block header_desc %}ただのTLS/SSL設定分析サービス{% endblock %} {% block extra_head %} {% endblock %} {% block content %} -
-

Scanning {{ target }}

-

Test ID: {{ test_id }}

-
-
-
-

waiting for queue…

-
-
-

Log

-
- {% for entry in progress_entries %} -
- [{{ entry.phase }}] - {{ entry.detail }} +
+

Nercone TLS Test

+

{{ target }} のテストは実行中です。

+ +
+

待機中…

+
+
- {% endfor %} +

テストID: {{ test_id }}

-
+ +
+
+ {% for entry in progress_entries %} +
+ [{{ entry.phase }}] + {{ entry.detail }} +
+ {% endfor %} +
+
+ +