This commit is contained in:
2026-04-19 11:33:11 +09:00
parent 867ae25fa0
commit da8d91b87f
43 changed files with 4044 additions and 15 deletions
+125
View File
@@ -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;
}
+118
View File
@@ -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();
})();
+41
View File
@@ -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 %}
<link rel="stylesheet" href="/tools/tls-test/assets/tls-test.css">
{% endblock %}
{% block content %}
<div class="block">
<h1>TLS Test</h1>
<p>任意のホストに対してTLS/SSL設定の詳細チェックを行い、SSS〜Rの21段階でランク付けします。</p>
<form method="POST" action="/tools/tls-test/" class="tls-form">
<label for="target" class="text-tx-alt font-small">対象ホスト (例: example.com / 192.0.2.1 / [2001:db8::1]:8443)</label>
<input type="text" id="target" name="target" required autocomplete="off" spellcheck="false"
value="{{ last_target or '' }}"
placeholder="example.com" class="tls-input">
<button type="submit" class="tls-submit">Start scan</button>
{% if error %}
<p class="text-bright-red font-small">{{ error }}</p>
{% endif %}
</form>
<p class="font-small text-tx-alt">プロトコルバージョン・暗号スイート・証明書・HSTS・CAA・HTTP/1-2-3 対応・主要脆弱性・ハンドシェイクシミュレーションを検査します。</p>
<p class="font-small text-tx-alt">IP 直接入力の場合、SNI/証明書名の一致を判定できないため減点対象となることがあります。</p>
</div>
<div class="block">
<h2>利用上の注意</h2>
<ul>
<li>このツールはとりあえず動けば良いやと、Claudeの性能チェックも兼ねて、仕様書っぽいやつ以外ほぼ丸ごと作らせたため、バグがいくつかあると思います。レイアウトがあまり良くない箇所とかもすでに何箇所か発見しています。今後改善予定です。</li>
<li>このツールは指定されたホストに対して<b>実際に TLS 接続を行います</b>。第三者のサーバーに対するスキャンは、対象サーバーの利用規約や法律を遵守した上で行ってください。</li>
<li>レート制限: 同一 IP から同時実行は 1 件、1 時間あたり 10 件までです。</li>
<li>結果は7日間保持されます。テストIDを知っている人は同じ結果を閲覧できます。</li>
<li>Nercone TLS Testサービスが使用するUser-Agent文字列は<code>nercone-tls-test/1.0</code>です。</li>
</ul>
</div>
<div class="block">
<h2>API</h2>
<p>同等の機能を JSON API として提供しています。</p>
<pre>POST /api/tools/tls-test/scan<br>Content-Type: application/json<br>{"target": "example.com"}</pre>
</div>
{% endblock %}
+100
View File
@@ -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 %}
<link rel="stylesheet" href="/tools/tls-test/assets/tls-test.css">
{% endblock %}
{% block content %}
{% set rank_color_map = {
'SSS': 'bright-green', 'SS': 'bright-green', 'S': 'bright-green',
'A': 'green', 'B': 'green', 'C': 'green',
'D': 'bright-yellow', 'E': 'bright-yellow', 'F': 'bright-yellow',
'G': 'yellow', 'H': 'yellow', 'I': 'yellow',
'J': 'bright-orange', 'K': 'bright-orange', 'L': 'bright-orange',
'M': 'orange', 'N': 'orange',
'O': 'bright-red', 'P': 'bright-red',
'Q': 'red', 'R': 'purple'
} %}
{% set rank_color = rank_color_map.get(result.rank or job.rank, 'tx') %}
<div class="block tls-summary">
<div class="tls-rank-badge text-{{ rank_color }}">
<span class="tls-rank-letters">{{ result.rank or job.rank or '?' }}</span>
<span class="tls-rank-score">{{ "%.2f"|format(result.score or job.score or 0) }}</span>
</div>
<div class="tls-summary-meta flex-1">
<h1 class="tls-target">{{ result.target or job.target }}</h1>
<p class="font-small text-tx-alt">host={{ result.host }} port={{ result.port }}</p>
<p class="font-small text-tx-alt">実施: {{ job.started_at | default("") }} / 所要時間: {{ "%.2f"|format(result.duration or 0) }}秒 / テストID: <code>{{ test_id }}</code></p>
<p class="font-small text-tx-alt">結果は7日間保持され、その後自動的に削除されます。</p>
{% if result.error %}
<p class="text-bright-red">{{ result.error }}</p>
{% endif %}
</div>
</div>
{% 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() %}
<div class="block">
<h2>{{ category_titles.get(cat_key, cat_key|capitalize) }}</h2>
{% for f in findings %}
<p class="tls-finding">
<span class="text-{{ f.color }}">[{{ f.severity_label }}]</span>
<span class="font-bold">{{ f.title }}</span>
{% if f.detail %}<span class="text-tx-alt font-small">— {{ f.detail }}</span>{% endif %}
</p>
{% endfor %}
</div>
{% endfor %}
{% if result.data and result.data.handshake_simulation %}
<div class="block">
<h2>Handshake Simulation</h2>
<table class="tls-table">
<thead>
<tr><th>Client</th><th>Version</th><th>Cipher</th></tr>
</thead>
<tbody>
{% for s in result.data.handshake_simulation %}
<tr>
<td>{{ s.client }}</td>
<td>
{% if s.connected %}
<span class="text-bright-green">{{ s.negotiated_version }}</span>
{% else %}
<span class="text-bright-red">failed</span>
{% endif %}
</td>
<td class="font-small text-tx-alt">
{% if s.connected %}{{ s.negotiated_cipher }}{% else %}{{ s.error }}{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="block">
<h2>Raw JSON</h2>
<p class="font-small text-tx-alt">API でも同じデータを取得できます: <code>GET /api/tools/tls-test/results/{{ test_id }}</code></p>
<details>
<summary>Show raw result JSON</summary>
<pre class="tls-raw"><code>{{ result | tojson(indent=2) }}</pre>
</details>
</div>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
{% extends "/base.html" %}
{% block title %}Scanning {{ target }} - Nercone TLS Test{% endblock %}
{% block title_suffix %}TLS Test{% endblock %}
{% block description %}スキャンの進捗を表示しています。{% endblock %}
{% block header_desc %}スキャン中です...{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/tools/tls-test/assets/tls-test.css">
{% endblock %}
{% block content %}
<div class="block">
<h1>Scanning <span class="text-bright-blue">{{ target }}</span></h1>
<p class="font-small text-tx-alt">Test ID: <code>{{ test_id }}</code></p>
<div class="tls-progress-track">
<div id="tls-progress-bar" class="tls-progress-bar" style="width: 0%"></div>
</div>
<p id="tls-phase" class="font-small text-tx-alt">waiting for queue…</p>
</div>
<div class="block">
<h2>Log</h2>
<div id="tls-log" class="tls-log">
{% for entry in progress_entries %}
<div class="tls-log-row">
<span class="text-tx-alt font-small">[{{ entry.phase }}]</span>
<span class="text-{{ {'good':'bright-green','normal':'bright-yellow','notgood':'bright-orange','bad':'bright-red','serious':'magenta','info':'tx'}.get(entry.severity, 'tx') }}">{{ entry.detail }}</span>
</div>
{% endfor %}
</div>
</div>
<script>
window.__TLS_INIT__ = {
id: "{{ test_id }}",
target: "{{ target }}",
status: "{{ status }}",
wsUrl: (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/tools/tls-test/ws/{{ test_id }}",
resultsUrl: "/tools/tls-test/results/{{ test_id }}/",
};
</script>
<script src="/tools/tls-test/assets/tls-test.js" defer></script>
{% endblock %}