+
+
Nercone TLS Test
+
仕様書っぽいやつ以外ほぼ全部Claudeに作らせたバグ多めで評価厳しめのTLSサーバーテストツール
+
tls-test
+
+
+{% endblock %}
diff --git a/public/tools/tls-test/assets/tls-test.css b/public/tools/tls-test/assets/tls-test.css
new file mode 100644
index 0000000..b168bc1
--- /dev/null
+++ b/public/tools/tls-test/assets/tls-test.css
@@ -0,0 +1,125 @@
+.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;
+ cursor: pointer;
+ font-family: inherit;
+}
+.tls-submit:hover {
+ border-color: #00C878;
+ color: #00C878;
+}
+
+.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;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border: 4px solid currentColor;
+ flex-shrink: 0;
+}
+.tls-rank-letters {
+ font-size: 48pt;
+ font-weight: 700;
+ line-height: 1;
+}
+.tls-rank-score {
+ font-size: 10pt;
+ color: #939393;
+ margin-top: 4px;
+}
+.tls-summary-meta {
+ min-width: 200px;
+}
+.tls-target {
+ margin: 0 0 8px 0;
+ word-break: break-all;
+}
+
+.tls-finding {
+ margin: 4px 0;
+ line-height: 1.5;
+}
+
+.tls-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 8px;
+}
+.tls-table th, .tls-table td {
+ border-bottom: 1px solid #3a3a3a;
+ padding: 6px 8px;
+ text-align: left;
+}
+.tls-table th {
+ color: #939393;
+ font-weight: 400;
+ font-size: 10pt;
+}
+
+.tls-raw {
+ max-height: 400px;
+ overflow: auto;
+ background-color: #202020;
+ padding: 12px;
+ border-radius: 6px;
+ font-size: 10pt;
+}
diff --git a/public/tools/tls-test/assets/tls-test.js b/public/tools/tls-test/assets/tls-test.js
new file mode 100644
index 0000000..d31234f
--- /dev/null
+++ b/public/tools/tls-test/assets/tls-test.js
@@ -0,0 +1,118 @@
+(function () {
+ const init = window.__TLS_INIT__;
+ if (!init) return;
+
+ const phaseEl = document.getElementById("tls-phase");
+ const barEl = document.getElementById("tls-progress-bar");
+ const logEl = document.getElementById("tls-log");
+
+ const SEV_COLOR = {
+ good: "bright-green",
+ normal: "bright-yellow",
+ notgood: "bright-orange",
+ bad: "bright-red",
+ serious: "magenta",
+ info: "tx",
+ };
+
+ let reconnectAttempts = 0;
+ const MAX_RECONNECTS = 3;
+ let ws = null;
+ let closedByDone = false;
+
+ function appendLog(phase, 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 msg = document.createElement("span");
+ msg.className = `text-${SEV_COLOR[severity] || "tx"}`;
+ msg.textContent = detail || "";
+ row.appendChild(label);
+ row.appendChild(msg);
+ logEl.appendChild(row);
+ logEl.scrollTop = logEl.scrollHeight;
+ }
+
+ function setProgress(value, phase) {
+ if (barEl) barEl.style.width = `${Math.max(0, Math.min(1, value)) * 100}%`;
+ if (phaseEl && phase) phaseEl.textContent = phase;
+ }
+
+ function connect() {
+ try {
+ ws = new WebSocket(init.wsUrl);
+ } catch (e) {
+ scheduleReconnect();
+ return;
+ }
+ ws.onmessage = (ev) => {
+ let msg;
+ try {
+ msg = JSON.parse(ev.data);
+ } catch (_) {
+ return;
+ }
+ if (msg.type === "history") {
+ (msg.entries || []).forEach((e) => appendLog(e.phase, e.detail, e.severity));
+ if (msg.status === "done") {
+ closedByDone = true;
+ location.replace(init.resultsUrl);
+ }
+ return;
+ }
+ if (msg.type === "progress") {
+ appendLog(msg.phase, msg.detail, msg.severity);
+ if (typeof msg.progress === "number") {
+ setProgress(msg.progress, msg.detail || msg.phase);
+ }
+ return;
+ }
+ 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");
+ return;
+ }
+ if (msg.type === "done") {
+ closedByDone = true;
+ setProgress(1.0, `done (rank ${msg.rank}, score ${msg.score})`);
+ location.replace(msg.redirect || init.resultsUrl);
+ return;
+ }
+ if (msg.type === "error") {
+ appendLog("error", msg.message || "engine failed", "serious");
+ return;
+ }
+ if (msg.type === "started") {
+ appendLog("started", msg.target || "", "info");
+ return;
+ }
+ };
+ ws.onclose = () => {
+ if (!closedByDone) scheduleReconnect();
+ };
+ ws.onerror = () => {
+ try { ws.close(); } catch (_) {}
+ };
+ }
+
+ function scheduleReconnect() {
+ if (closedByDone) return;
+ if (reconnectAttempts >= MAX_RECONNECTS) {
+ appendLog("ws", "WebSocket 接続が切断されました。ページをリロードしてください。", "bad");
+ return;
+ }
+ const delay = Math.min(10_000, 1000 * Math.pow(2, reconnectAttempts));
+ reconnectAttempts += 1;
+ setTimeout(connect, delay);
+ }
+
+ window.addEventListener("beforeunload", () => {
+ closedByDone = true;
+ try { ws && ws.close(); } catch (_) {}
+ });
+
+ connect();
+})();
diff --git a/public/tools/tls-test/index.html b/public/tools/tls-test/index.html
new file mode 100644
index 0000000..7fdffbb
--- /dev/null
+++ b/public/tools/tls-test/index.html
@@ -0,0 +1,41 @@
+{% extends "/base.html" %}
+{% block title %}Nercone TLS Test{% endblock %}
+{% block title_suffix %}TLS Test{% endblock %}
+{% block description %}任意のホストに対して TLS/SSL 設定の詳細チェックとランク付けを行います。{% endblock %}
+{% block header_desc %}ただのTLS/SSL設定分析サービス{% endblock %}
+{% block extra_head %}
+
+
TLS Test
+
任意のホストに対してTLS/SSL設定の詳細チェックを行い、SSS〜Rの21段階でランク付けします。
+
+
プロトコルバージョン・暗号スイート・証明書・HSTS・CAA・HTTP/1-2-3 対応・主要脆弱性・ハンドシェイクシミュレーションを検査します。
+
IP 直接入力の場合、SNI/証明書名の一致を判定できないため減点対象となることがあります。
+
+
+
API
+
同等の機能を JSON API として提供しています。
+
POST /api/tools/tls-test/scan
Content-Type: application/json
{"target": "example.com"}
+
+{% endblock %}
diff --git a/public/tools/tls-test/results.html b/public/tools/tls-test/results.html
new file mode 100644
index 0000000..bd1213a
--- /dev/null
+++ b/public/tools/tls-test/results.html
@@ -0,0 +1,100 @@
+{% 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 extra_head %}
+
+
+ {{ result.rank or job.rank or '?' }}
+ {{ "%.2f"|format(result.score or job.score or 0) }}
+
+
+
+
+ {% 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 %}
+