This commit is contained in:
2026-04-25 23:11:20 +09:00
parent 209b14e9f8
commit a4e31d1641
43 changed files with 14 additions and 5421 deletions
+1 -1
View File
@@ -346,7 +346,7 @@ initProgressiveHeaderBlur();
}
})();
/* Loading Animation */
/* Loading Overlay */
(() => {
const overlay = document.getElementById('loading-overlay');
if (!overlay) return;
-1
View File
@@ -82,7 +82,6 @@
<a href="/about/" class="text-no-decoration bold-on-small">about</a>
<a href="/links/" class="text-no-decoration bold-on-small">links</a>
<a href="/projects/" class="text-no-decoration bold-on-small">projects</a>
<a href="/tools/" class="text-no-decoration hide show-on-large">tools</a>
<a href="{{ onion_site_url }}" class="text-no-decoration hide show-on-large">onion</a>
<a href="mailto:nercone@nercone.dev" class="text-no-decoration hide show-on-large">email</a>
<a href="/public-key/" class="text-no-decoration hide show-on-large">public-key</a>
-9
View File
@@ -52,13 +52,4 @@
<a href="https://github.com/nercone-dev/zeta-llm/" class="text-no-decoration">github</a>
</div>
</div>
<div id="tls-test" class="block">
<h3 style="margin-bottom: 0px;">Nercone TLS Test</h3>
<p style="margin-top: 0px;">TLS設定を分析して評価するサービス</p>
<div class="flex font-small text-light-grey-alt">
<a href="#tls-test" class="text-no-decoration font-bold">tls-test</a>
<a href="/tools/tls-test/" class="text-no-decoration">website</a>
<a href="https://gitea.nercone.dev/nercone-dev/website/src/branch/main/src/nercone_website/tools/tls_test/" class="text-no-decoration">gitea</a>
</div>
</div>
{% endblock %}
-14
View File
@@ -1,14 +0,0 @@
{% extends "/base.html" %}
{% block title %}Tools - Nercone{% endblock %}
{% block title_suffix %}Tools{% endblock %}
{% block header_desc %}nercone.dev上で使用可能なツールたち{% endblock %}
{% block description %}nercone.dev上で使用可能なツールたち{% endblock %}
{% block content %}
<a href="/tools/tls-test/" class="text-no-decoration font-bold">
<div id="tls-test" class="block">
<h3 style="margin-bottom: 0px;">Nercone TLS Test</h3>
<p style="margin-top: 0px;">任意のホストに対して TLS/SSL 設定の詳細チェックとランク付けを行うWebサービス</p>
<span class="flex font-small text-light-grey-alt">tls-test</span>
</div>
</a>
{% endblock %}
@@ -1,102 +0,0 @@
(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);
}
}
// ---- Shared copy helper (clipboard API + textarea fallback) ----
async function copyText(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
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);
return true;
} catch (_) {
return false;
}
}
// Swap a button's label and is-done class for 1.5s based on action result.
function bindCopyButton(btn, getText) {
if (!btn) return;
const orig = btn.textContent;
btn.addEventListener("click", async () => {
const ok = await copyText(getText());
btn.textContent = ok ? "コピーしました" : "コピー失敗";
if (ok) btn.classList.add("is-done");
setTimeout(() => {
btn.textContent = orig;
btn.classList.remove("is-done");
}, 1500);
});
}
// ---- JSON-copy button (inside the JSON tab) ----
const rawJsonEl = document.getElementById("tls-raw-json");
bindCopyButton(document.getElementById("tls-copy-json"), () => (rawJsonEl && rawJsonEl.textContent) || "");
// ---- Copy-link button ----
const copyLinkBtn = document.getElementById("tls-copy-link");
bindCopyButton(copyLinkBtn, () => (copyLinkBtn && copyLinkBtn.dataset.link) || location.href);
// ---- 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();
}
});
}
})();
-471
View File
@@ -1,471 +0,0 @@
/* Nercone TLS Test — shared styles. Palette uses CSS vars from /assets/css/style.css. */
/* ------ Landing page ------ */
.tls-landing {
min-height: calc(100vh - 220px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
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;
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: var(--color-light-grey);
border: 1px solid #4a4a4a;
border-radius: 6px;
padding: 12px 16px;
font-family: inherit;
font-size: 13pt;
}
.tls-landing-input::placeholder { color: var(--color-light-grey-alt); }
.tls-landing-input:focus {
outline: none;
border-color: var(--color-bright-blue);
}
.tls-landing-submit {
background-color: #0096D0;
color: var(--color-white);
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: var(--color-light-grey-alt);
text-decoration: underline;
}
.tls-landing-links a:hover { color: var(--color-light-grey); }
.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-progress-track {
width: 100%;
height: 6px;
background-color: #2a2a2a;
border-radius: 3px;
overflow: hidden;
}
.tls-progress-bar {
height: 100%;
background-color: var(--color-bright-green);
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: var(--color-dark-grey-alt);
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 https://assets.nercone.dev/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 <main> carries to clear the (now absent) floating site header. */
.tls-results-header {
position: static;
display: block;
background-color: var(--color-dark-grey-alt);
padding: 28px clamp(16px, 4vw, 40px) 0;
margin-bottom: 0;
border-bottom: 1px solid var(--color-dark-grey);
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: 30pt;
font-weight: 600;
line-height: 1;
letter-spacing: -0.02em;
}
.tls-rank-score {
font-size: 10pt;
font-weight: 600;
margin-top: 2px;
opacity: 0.75;
}
.tls-results-meta {
min-width: 220px;
}
.tls-results-target {
margin: 0 0 4px 0;
font-size: 22pt;
font-weight: 700;
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 {
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: var(--color-light-grey);
}
.tls-btn-secondary:hover { background-color: #4a4a4a; }
.tls-btn-primary {
background-color: #0096D0;
color: var(--color-white);
}
.tls-btn-primary:hover { background-color: #00B4F0; }
.tls-btn-primary.is-done { background-color: var(--color-green); }
/* 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 var(--color-dark-grey);
}
.tls-tab {
background: transparent;
color: var(--color-light-grey-alt);
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: var(--color-light-grey);
}
.tls-tab.is-active {
color: var(--color-white);
font-weight: 600;
border-bottom-color: var(--color-light-grey);
}
.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;
margin: 0 0 10px 0;
padding-bottom: 6px;
border-bottom: 1px solid #3a3a3a;
}
/* Standard result tables. */
.tls-table {
width: 100%;
border-collapse: collapse;
font-size: 11pt;
background-color: #202020;
border-radius: 6px;
overflow: hidden;
color: var(--color-light-grey);
}
.tls-table th,
.tls-table td {
padding: 10px 12px;
text-align: left;
vertical-align: top;
border-bottom: 1px solid #2c2c2c;
}
.tls-table thead th {
background-color: #2a2a2a;
color: var(--color-light-grey-alt);
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-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: var(--color-light-grey);
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-size: 10pt;
color: var(--color-light-grey);
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: 640px;
overflow: auto;
background-color: #202020;
padding: 14px 16px;
border-radius: 6px;
font-size: 10pt;
margin: 0;
}
.tls-raw code {
white-space: pre;
background: transparent;
padding: 0;
}
/* Print stylesheet — "PDFをダウンロード" uses window.print(). Hide chrome and
* expand all tab panels so the resulting PDF contains every finding. */
@media print {
html, body {
background: #ffffff !important;
color: #101010 !important;
}
footer, .tls-tabs, .tls-results-actions,
#tls-copy-json, script, .tls-landing-links {
display: none !important;
}
.tls-results-header {
background: transparent !important;
border: 0 !important;
padding: 0 0 16px 0 !important;
border-bottom: 1px solid #cccccc !important;
}
.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;
}
.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;
}
}
-165
View File
@@ -1,165 +0,0 @@
(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 verbEl = document.getElementById("tls-status-verb");
// Severity label tag. Matches SEVERITY_COLORS in schemas.py so the live
// log and results page render identically.
const SEV = {
good: { color: "bright-green", label: "GOOD" },
normal: { color: "bright-blue", label: "NORMAL" },
notgood: { color: "bright-yellow", label: "NOT GOOD" },
bad: { color: "bright-red", label: "BAD" },
serious: { color: "bright-purple", label: "SERIOUS" },
info: { color: "light-grey-alt", label: "INFO" },
};
let reconnectAttempts = 0;
const MAX_RECONNECTS = 3;
let ws = null;
let closedByDone = false;
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 cat = document.createElement("span");
cat.className = "tls-log-cat text-light-grey-alt";
cat.textContent = "[" + normalizeStep(category) + "]";
row.appendChild(cat);
const msg = document.createElement("span");
msg.className = "tls-log-msg";
if (severity && SEV[severity] && severity !== "info") {
const sev = document.createElement("span");
sev.className = "text-" + SEV[severity].color + " font-bold";
sev.textContent = SEV[severity].label;
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-light-grey";
msgText.textContent = body || "";
msg.appendChild(msgText);
row.appendChild(msg);
const det = document.createElement("span");
det.className = "tls-log-detail text-light-grey-alt";
det.textContent = detail || "";
row.appendChild(det);
logEl.appendChild(row);
logEl.scrollTop = logEl.scrollHeight;
}
function setProgress(value, phaseText) {
if (barEl) barEl.style.width = `${Math.max(0, Math.min(1, value)) * 100}%`;
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() {
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) => appendRow(e.phase, e.detail, "", e.severity));
if (msg.status === "done") {
closedByDone = true;
typeof window.__navigate === 'function'
? window.__navigate(init.resultsUrl)
: location.replace(init.resultsUrl);
}
return;
}
if (msg.type === "progress") {
// 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);
}
return;
}
if (msg.type === "finding") {
const f = msg.finding || {};
// 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;
markDone(msg.rank, msg.score);
setTimeout(() => {
const target = msg.redirect || init.resultsUrl;
typeof window.__navigate === 'function'
? window.__navigate(target)
: location.replace(target);
}, 300);
return;
}
if (msg.type === "error") {
appendRow("error", msg.message || "engine failed", "", "serious");
return;
}
if (msg.type === "started") {
appendRow("init", msg.target || "", "", "info");
return;
}
};
ws.onclose = () => {
if (!closedByDone) scheduleReconnect();
};
ws.onerror = () => {
try { ws.close(); } catch (_) {}
};
}
function scheduleReconnect() {
if (closedByDone) return;
if (reconnectAttempts >= MAX_RECONNECTS) {
appendRow("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();
})();
-50
View File
@@ -1,50 +0,0 @@
{% extends "/base.html" %}
{% block title %}Nercone TLS Test{% endblock %}
{% block title_suffix %}TLS Test{% endblock %}
{% block description %}Nercone TLS Testは、任意のホストに対して TLS/SSL 設定の詳細チェックとランク付けを行うWebサービスです。{% endblock %}
{% block header_desc %}ただのTLS/SSL設定分析サービス{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/tools/tls-test/assets/tls-test.css">
{% endblock %}
{% block content %}
<section class="tls-landing">
<h1 class="tls-landing-title"><span class="font-bold">Nercone</span> <span class="font-weight-300">TLS Test</span></h1>
<p class="tls-landing-subtitle text-light-grey-alt">ただのTLS/SSL設定分析サービス</p>
<form method="POST" action="/tools/tls-test/" class="tls-landing-form">
<input type="text" name="target" required autocomplete="off" spellcheck="false"
value="{{ last_target or '' }}"
placeholder="nercone.dev" class="tls-landing-input"
aria-label="対象ホスト">
<button type="submit" class="tls-landing-submit">実行</button>
</form>
{% if error %}
<p class="tls-landing-error text-bright-red font-small">{{ error }}</p>
{% endif %}
<p class="tls-landing-links">
<a href="#notes" class="text-light-grey-alt text-underline">注意事項</a>
<a href="#api" class="text-light-grey-alt text-underline">API</a>
</p>
</section>
<section id="notes" class="tls-aux-section">
<div class="block">
<h2 class="font-large">注意事項</h2>
<ul>
<li>このツールは指定されたホストに対して実際にTLS接続を行いテストします。第三者のサーバーに対するスキャンは、対象サーバーの利用規約や法律を遵守した上で行ってください。</li>
<li>同一IPからの同時実行はできません。レートリミットは1時間あたり30件までです。</li>
<li>結果は最長1年間保存されます。少なくとも7日間保存され、経過後は件数が多い場合にのみ古い順に順次削除されます。</li>
<li>ドメイン名 / サブドメイン / IPv4 / IPv6 / ホスト名:ポート の形式を使用可能です。IPアドレスを使用した場合、SNI/証明書名の一致を判定できないため減点対象となることがあります。</li>
<li>Nercone TLS Testがサーバーへのアクセス時に使用するUser-Agent文字列は<code>nercone-tls-test/1.0</code>です。</li>
</ul>
</div>
</section>
<section id="api" class="tls-aux-section">
<div class="block">
<p>APIからも同等の機能が使用可能です。APIはJSON形式で返答します。</p>
<pre>POST /api/tools/tls-test/scan<br>Content-Type: application/json<br>{"target": "example.com"}</pre>
<p>次のエンドポイントで進捗・結果を取得することができます。</p>
<pre>GET /api/tools/tls-test/status/{test_id}<br>GET /api/tools/tls-test/results/{test_id}</pre>
</div>
</section>
{% endblock %}
-441
View File
@@ -1,441 +0,0 @@
{% extends "/base.html" %}
{% block title %}{{ result.target }} (ランク{{ result.rank }}) - Nercone TLS Test{% endblock %}
{% block title_suffix %}TLS Test{% 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 %}
<link rel="stylesheet" href="/tools/tls-test/assets/tls-test.css">
{% endblock %}
{# ------------------------------------------------------------------
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',
'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': 'bright-purple'
} %}
{% set rank = result.rank or job.rank or '?' %}
{% set rank_color = rank_color_map.get(rank, 'light-grey') %}
<header class="tls-results-header">
<div class="tls-results-head">
<div class="tls-rank-badge text-{{ rank_color }}" aria-label="ランク {{ rank }}, スコア {{ '%.0f'|format(result.score or job.score or 0) }}">
<span class="tls-rank-letters">{{ rank }}</span>
<span class="tls-rank-score">{{ "%.0f"|format(result.score or job.score or 0) }}</span>
</div>
<div class="tls-results-meta flex-1">
<h1 class="tls-results-target">{{ result.target or job.target }}</h1>
<p class="tls-results-metaline">
<span class="text-light-grey-alt">実行日時</span>
<span>{{ finished_at_display or job.finished_at or "—" }}</span>
<span class="text-light-grey-alt">所要時間</span>
<span>{{ "%.0f"|format((result.duration or 0) * 1000) }}ms</span>
</p>
<p class="tls-results-testid"><code class="text-light-grey-alt">{{ test_id }}</code></p>
{% if result.error %}
<p class="text-bright-red">{{ result.error }}</p>
{% endif %}
</div>
<div class="tls-results-actions">
<button type="button" class="tls-btn tls-btn-secondary" id="tls-print-pdf">PDFをダウンロード</button>
<button type="button" class="tls-btn tls-btn-primary" id="tls-copy-link" data-link="{{ share_url }}">リンクをコピー</button>
</div>
</div>
<nav class="tls-tabs" role="tablist">
<button type="button" class="tls-tab is-active" data-tab="summary" role="tab" aria-selected="true">概要</button>
<button type="button" class="tls-tab" data-tab="reliability" role="tab" aria-selected="false">信頼性</button>
<button type="button" class="tls-tab" data-tab="safety" role="tab" aria-selected="false">安全性</button>
<button type="button" class="tls-tab" data-tab="vulnerabilities" role="tab" aria-selected="false">脆弱性</button>
<button type="button" class="tls-tab" data-tab="compatibility" role="tab" aria-selected="false">互換性</button>
<button type="button" class="tls-tab" data-tab="log" role="tab" aria-selected="false">ログ</button>
<button type="button" class="tls-tab" data-tab="history" role="tab" aria-selected="false">テスト履歴</button>
<button type="button" class="tls-tab" data-tab="json" role="tab" aria-selected="false">JSON</button>
</nav>
</header>
{% endblock %}
{% block content %}
{#
Log row for a finding. Columns:
[step] severity-label + title(white) detail(gray)
#}
{% set _color_compat = {'tx': 'light-grey', 'tx-alt': 'light-grey-alt'} %}
{% macro finding_row(f) -%}
<div class="tls-log-row">
<span class="tls-log-cat text-light-grey-alt">[{{ (f.step or f.group or f.category or 'info')|replace('_','-')|lower }}]</span>
<span class="tls-log-msg">
<span class="text-{{ _color_compat.get(f.color, f.color) }} font-bold">{{ f.severity_label|upper }}</span>
<span class="text-light-grey">{{ f.title }}</span>
</span>
<span class="tls-log-detail text-light-grey-alt">{{ f.detail or '' }}</span>
</div>
{%- endmacro %}
{# A boolean row rendered with green/red text. #}
{% macro yn(value, ok='対応', bad='未対応') -%}
{% if value %}<span class="text-bright-green">{{ ok }}</span>{% else %}<span class="text-bright-red">{{ bad }}</span>{% 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 -%}
<span class="text-light-grey-alt"></span>
{%- elif r.checked and not r.revoked -%}
<span class="text-bright-green">失効していません</span>
{%- if r.source %} <span class="font-small text-light-grey-alt">({{ r.source }})</span>{% endif -%}
{%- elif r.checked and r.revoked -%}
<span class="text-bright-red">失効しています</span>
{%- if r.reason %} <span class="font-small text-light-grey-alt">({{ r.reason }})</span>{% endif -%}
{%- else -%}
{%- set err = (r.error or '')|string -%}
{%- if 'no OCSP URL' in err or 'no AIA' in err -%}
<span class="text-light-grey-alt">{{ kind }} URL が提供されていません</span>
{%- elif 'no CRL DP' in err or 'no http CRL URL' in err -%}
<span class="text-light-grey-alt">{{ kind }} URL が提供されていません</span>
{%- elif 'all CRLs unreachable' in err -%}
<span class="text-bright-yellow">{{ kind }} レスポンダに到達できません</span>
{%- elif err -%}
<span class="text-bright-yellow">確認失敗</span>
<span class="font-small text-light-grey-alt">({{ err }})</span>
{%- else -%}
<span class="text-light-grey-alt">未確認</span>
{%- endif -%}
{%- endif -%}
{%- endmacro %}
<div class="tls-tab-panels">
{# -------- 概要 -------- #}
<div class="tls-tab-panel is-active" data-panel="summary">
<div class="tls-log">
{% if summary %}
{% for f in summary %}{{ finding_row(f) }}{% endfor %}
{% else %}
<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">該当項目なし</span></div>
{% endif %}
</div>
</div>
{# -------- 信頼性 -------- #}
<div class="tls-tab-panel" data-panel="reliability">
{# Certificate chain table #}
{% set chain = result.data.certificate_chain if result.data else [] %}
{% if chain %}
<section class="tls-section">
<h2 class="tls-section-title">証明書チェーン</h2>
<table class="tls-table">
<thead><tr>
<th>#</th><th>CN / Subject</th><th>発行者</th><th>有効期限</th><th>署名</th><th></th>
</tr></thead>
<tbody>
{% for c in chain %}
<tr>
<td class="text-light-grey-alt">{{ loop.index }}</td>
<td>
<div>{{ c.common_name or c.subject or '—' }}</div>
{% if c.sans %}<div class="font-small text-light-grey-alt">SAN: {{ (c.sans or [])|join(', ') }}</div>{% endif %}
</td>
<td>{{ c.issuer or '—' }}</td>
<td>
<div>{{ c.not_after or '—' }}</div>
{% if c.is_expired %}<span class="text-bright-red font-bold">期限切れ</span>
{% elif c.days_until_expiry is defined and c.days_until_expiry is not none %}<span class="font-small text-light-grey-alt">残 {{ c.days_until_expiry }} 日</span>{% endif %}
</td>
<td>{{ (c.signature_hash_algorithm or '—')|upper }}</td>
<td>
{{ c.public_key_algorithm or '—' }}{% if c.public_key_size_bits %} {{ c.public_key_size_bits }}-bit{% endif %}
{% if c.public_key_curve %}<span class="font-small text-light-grey-alt"> ({{ c.public_key_curve }})</span>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endif %}
{# Trust stores #}
{% set trust = result.data.trust if result.data else [] %}
{% if trust %}
<section class="tls-section">
<h2 class="tls-section-title">プラットフォームごとの信頼状況</h2>
<table class="tls-table">
<thead><tr><th>プラットフォーム</th><th>信頼</th><th>状況</th></tr></thead>
<tbody>
{% for t in trust %}
<tr>
<td>{{ t.platform }}</td>
<td>{{ yn(t.trusted, '信頼', '未信頼') }}</td>
<td class="text-light-grey-alt">{{ t.error or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% 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') %}
<section class="tls-section">
<h2 class="tls-section-title">失効・HSTS・CAA</h2>
<table class="tls-table">
<tbody>
<tr><th>OCSP</th><td>{{ rev_cell(ocsp, 'OCSP') }}</td></tr>
<tr><th>CRL</th><td>{{ rev_cell(crl, 'CRL') }}</td></tr>
<tr><th>HSTS</th><td>
{%- if not hsts or hsts.error -%}
<span class="text-light-grey-alt"></span>
{%- elif hsts.present -%}
<span class="text-bright-green">有効</span>
<span class="font-small text-light-grey-alt">max-age={{ hsts.max_age }}{% if hsts.include_subdomains %}; includeSubDomains{% endif %}{% if hsts.preload %}; preload{% endif %}</span>
{%- else -%}
<span class="text-bright-red">無効</span>
{%- endif -%}
</td></tr>
<tr><th>HSTS preload</th><td>
{%- if preload -%}
{%- for p in preload -%}
<span class="font-small">{{ p.browser }}: {% if p.error %}<span class="text-light-grey-alt">取得エラー</span>{% else %}{{ yn(p.listed, 'Listed', 'Not listed') }}{% endif %}</span>{% if not loop.last %} · {% endif %}
{%- endfor -%}
{%- else -%}
<span class="text-light-grey-alt"></span>
{%- endif -%}
</td></tr>
<tr><th>CAA</th><td>
{%- if not caa -%}
<span class="text-light-grey-alt"></span>
{%- elif caa.records -%}
<span class="text-bright-green">{{ caa.records|length }} 件</span>
<span class="font-small text-light-grey-alt">({{ caa.effective_host }})</span>
<div class="font-small text-light-grey-alt">{{ caa.records|join('; ') }}</div>
{%- else -%}
<span class="text-bright-yellow">未設定</span>
{%- endif -%}
</td></tr>
</tbody>
</table>
</section>
<section class="tls-section">
<h2 class="tls-section-title">信頼性に関するすべてのログ</h2>
<div class="tls-log">
{% set findings = groups['reliability'] %}
{% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %}
{% else %}<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">項目なし</span></div>{% endif %}
</div>
</section>
</div>
{# -------- 安全性 -------- #}
<div class="tls-tab-panel" data-panel="safety">
{# Protocol versions #}
{% set versions = (result.data or {}).get('versions') %}
{% if versions %}
<section class="tls-section">
<h2 class="tls-section-title">対応プロトコル</h2>
<table class="tls-table">
<thead><tr><th>バージョン</th><th>対応</th></tr></thead>
<tbody>
{% for name, ok in versions.items() %}
<tr><td>{{ name }}</td><td>{{ yn(ok, '有効', '無効') }}</td></tr>
{% endfor %}
</tbody>
</table>
</section>
{% endif %}
{# Accepted ciphers per version #}
{% set ciphers = (result.data or {}).get('ciphers') %}
{% if ciphers %}
<section class="tls-section">
<h2 class="tls-section-title">受理された暗号スイート</h2>
{% for name, cs in ciphers.items() %}
{% if cs %}
<div class="tls-cipher-block">
<h3 class="tls-cipher-version">{{ name }} <span class="font-small text-light-grey-alt">({{ cs|length }})</span></h3>
<ul class="tls-cipher-list">
{% for c in cs %}
<li><code>{{ c }}</code></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
</section>
{% endif %}
{# Named groups #}
{% set groups_list = (result.data or {}).get('named_groups') %}
{% if groups_list %}
<section class="tls-section">
<h2 class="tls-section-title">TLS 1.3 鍵交換グループ</h2>
<p>{% for g in groups_list %}<code>{{ g }}</code>{% if not loop.last %}, {% endif %}{% endfor %}</p>
</section>
{% endif %}
<section class="tls-section">
<h2 class="tls-section-title">安全性に関するすべてのログ</h2>
<div class="tls-log">
{% set findings = groups['safety'] %}
{% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %}
{% else %}<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">項目なし</span></div>{% endif %}
</div>
</section>
</div>
{# -------- 脆弱性 -------- #}
<div class="tls-tab-panel" data-panel="vulnerabilities">
{% 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') %}
<section class="tls-section">
<h2 class="tls-section-title">既知脆弱性の判定</h2>
<table class="tls-table">
<thead><tr><th>脆弱性</th><th>判定</th><th>詳細</th></tr></thead>
<tbody>
<tr><td>Heartbleed</td><td>{% if hb %}{{ yn(not hb.vulnerable, '影響なし', '影響あり') }}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td><td class="text-light-grey-alt">{% if hb %}{% if hb.heartbeat_extension %}Heartbeat advertised{% endif %}{% if hb.error %} {{ hb.error }}{% endif %}{% endif %}</td></tr>
<tr><td>CCS Injection</td><td>{% if ccs %}{{ yn(not ccs.vulnerable, '影響なし', '影響あり') }}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td><td class="text-light-grey-alt">{{ (ccs or {}).get('detail','') }}</td></tr>
<tr><td>Secure Renegotiation</td><td>{% if renego %}{{ yn(renego.supported, '対応', '未対応') }}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td><td class="text-light-grey-alt">{{ (renego or {}).get('detail','') }}</td></tr>
<tr><td>TLS_FALLBACK_SCSV</td><td>{% if scsv %}{{ yn(scsv.supported, '対応', '未対応') }}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td><td class="text-light-grey-alt">{{ (scsv or {}).get('detail','') }}</td></tr>
</tbody>
</table>
</section>
<section class="tls-section">
<h2 class="tls-section-title">脆弱性に関するすべてのログ</h2>
<div class="tls-log">
{% set findings = groups['vulnerabilities'] %}
{% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %}
{% else %}<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">項目なし</span></div>{% endif %}
</div>
</section>
</div>
{# -------- 互換性 -------- #}
<div class="tls-tab-panel" data-panel="compatibility">
{% set http = (result.data or {}).get('http') %}
{% set http3 = (result.data or {}).get('http3') %}
{% set alpn = (result.data or {}).get('alpn') %}
<section class="tls-section">
<h2 class="tls-section-title">HTTP プロトコル対応</h2>
<table class="tls-table">
<tbody>
<tr><th>HTTP/1.1</th><td>{% if http %}{{ yn(http.http1, '対応', '未対応') }}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td></tr>
<tr><th>HTTP/2</th><td>{% if http %}{{ yn(http.http2, '対応', '未対応') }}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td></tr>
<tr><th>HTTP/3 (QUIC)</th><td>{% if http3 %}{{ yn(http3.supported, '対応', '未対応') }}{% if http3.error %} <span class="font-small text-light-grey-alt">{{ http3.error }}</span>{% endif %}{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td></tr>
<tr><th>ALPN</th><td>{% if alpn %}<code>{{ alpn }}</code>{% else %}<span class="text-light-grey-alt"></span>{% endif %}</td></tr>
{% if http and http.server %}<tr><th>Server</th><td><code>{{ http.server }}</code></td></tr>{% endif %}
{% if http and http.alt_svc %}<tr><th>Alt-Svc</th><td><code>{{ http.alt_svc }}</code></td></tr>{% endif %}
</tbody>
</table>
</section>
{% set sim = (result.data or {}).get('handshake_simulation') %}
{% if sim %}
<section class="tls-section">
<h2 class="tls-section-title">ハンドシェイクシミュレーション</h2>
<table class="tls-table">
<thead><tr><th>クライアント</th><th>結果</th><th>プロトコル</th><th>暗号</th><th>備考</th></tr></thead>
<tbody>
{% for s in sim %}
<tr>
<td>{{ s.client }}</td>
<td>{{ yn(s.connected, 'OK', 'Fail') }}</td>
<td>{{ s.negotiated_version or '—' }}</td>
<td><code class="font-small">{{ s.negotiated_cipher or '—' }}</code></td>
<td class="font-small text-light-grey-alt">{{ s.error or '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endif %}
<section class="tls-section">
<h2 class="tls-section-title">互換性に関するすべてのログ</h2>
<div class="tls-log">
{% set findings = groups['compatibility'] %}
{% if findings %}{% for f in findings %}{{ finding_row(f) }}{% endfor %}
{% else %}<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">項目なし</span></div>{% endif %}
</div>
</section>
</div>
{# -------- ログ --------
Replays the live processing log: interleaved progress rows
(from test_progress table) and findings (grouped by step). #}
<div class="tls-tab-panel" data-panel="log">
<section class="tls-section">
<div class="tls-log tls-log-replay">
{% if log_entries %}
{% for e in log_entries %}
{% if e.kind == 'phase' %}
<div class="tls-log-row">
<span class="tls-log-cat text-light-grey-alt">[{{ (e.phase or 'info')|replace('_','-')|lower }}]</span>
<span class="tls-log-msg text-light-grey">{{ e.detail or '' }}</span>
<span class="tls-log-detail text-light-grey-alt"></span>
</div>
{% else %}
{{ finding_row(e.finding) }}
{% endif %}
{% endfor %}
{% else %}
<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">ログが記録されていません</span></div>
{% endif %}
</div>
</section>
</div>
{# -------- テスト履歴 -------- #}
<div class="tls-tab-panel" data-panel="history">
<section class="tls-section">
<h2 class="tls-section-title">過去のテスト結果</h2>
{% if history %}
<table class="tls-table">
<thead><tr><th>実行日時</th><th>ランク</th><th>スコア</th><th>リンク</th></tr></thead>
<tbody>
{% for h in history %}
<tr>
<td>{{ h.finished_at_display or '—' }}</td>
<td>{{ h.rank or '—' }}</td>
<td>{{ "%.0f"|format(h.score or 0) }}</td>
<td><a href="/tools/tls-test/results/{{ h.id }}/" class="text-light-grey-alt">{{ h.id }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="tls-log-row"><span class="tls-log-cat text-light-grey-alt">[info]</span><span class="tls-log-msg text-light-grey-alt">過去のテスト結果はありません</span></div>
{% endif %}
</section>
</div>
{# -------- JSON -------- #}
<div class="tls-tab-panel" data-panel="json">
<section class="tls-section">
<div class="tls-json-meta">
<code class="text-light-grey-alt">GET /api/tools/tls-test/results/{{ test_id }}</code>
<button type="button" class="tls-btn tls-btn-secondary" id="tls-copy-json">JSONをコピー</button>
</div>
<pre class="tls-raw" id="tls-raw-json"><code>{{ result | tojson(indent=2) }}</code></pre>
</section>
</div>
</div>
<script src="/tools/tls-test/assets/tls-test-results.js" defer></script>
{% endblock %}
-44
View File
@@ -1,44 +0,0 @@
{% extends "/base.html" %}
{% block title %}{{ target }} をテスト中 - Nercone TLS Test{% endblock %}
{% block title_suffix %}TLS Test{% endblock %}
{% block description %}{{ target }} に対する TLS Test の進捗を表示しています。{% endblock %}
{% block header_desc %}ただのTLS/SSL設定分析サービス{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/tools/tls-test/assets/tls-test.css">
{% endblock %}
{% block content %}
<section class="tls-status">
<h1 class="tls-landing-title"><span class="font-bold">Nercone</span> <span class="font-weight-300">TLS Test</span></h1>
<p class="tls-status-subtitle"><span class="font-bold">{{ target }}</span><span class="text-light-grey-alt"> のテストは<span id="tls-status-verb">実行中</span>です。</span></p>
<div class="tls-status-progress">
<p id="tls-phase" class="tls-status-phase text-light-grey-alt font-small">待機中…</p>
<div class="tls-progress-track">
<div id="tls-progress-bar" class="tls-progress-bar" style="width: 0%"></div>
</div>
<p class="tls-status-testid text-light-grey-alt font-small">テストID: <code>{{ test_id }}</code></p>
</div>
<div class="tls-log-wrap">
<div id="tls-log" class="tls-log">
{% for entry in progress_entries %}
<div class="tls-log-row">
<span class="tls-log-cat text-light-grey-alt">[{{ entry.phase }}]</span>
<span class="tls-log-msg">{{ entry.detail }}</span>
</div>
{% endfor %}
</div>
</div>
</section>
<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 %}