This commit is contained in:
2026-04-19 18:54:47 +09:00
parent 481a6288bf
commit 1acab0917b
21 changed files with 1976 additions and 586 deletions
+102 -7
View File
@@ -145,16 +145,35 @@ async def thumbnail(request: Request, path: str) -> Response:
return Response(content=png, media_type="image/png")
def _validate_tls_target(raw: str) -> str | None:
import ipaddress
s = (raw or "").strip()
if not s or not re.compile(r"^[A-Za-z0-9._:\[\]\-]{1,255}$").match(s):
if not s or len(s) > 255:
return None
# Reject obviously garbage input (all dashes/dots, control chars, whitespace).
if not re.compile(r"^[A-Za-z0-9._:\[\]\-]{1,255}$").match(s):
return None
try:
host, port, _ = parse_target(s)
except Exception:
return None
if not host:
if not host or port <= 0 or port > 65535:
return None
if port <= 0 or port > 65535:
# Host must be either an IP literal or a valid-looking hostname.
try:
ipaddress.ip_address(host)
return s
except ValueError:
pass
# Hostname: each label must be 1-63 chars, not start/end with '-', no empty labels.
labels = host.split(".")
if not labels or any(not lbl for lbl in labels):
return None
for lbl in labels:
if len(lbl) > 63 or lbl.startswith("-") or lbl.endswith("-"):
return None
if not re.match(r"^[A-Za-z0-9\-]+$", lbl):
return None
if len(labels) < 2 and host != "localhost":
return None
return s
@@ -213,9 +232,74 @@ async def tls_test_results_page(request: Request, test_id: str) -> Response:
if job.get("status") != "done":
return RedirectResponse(url=f"/tools/tls-test/status/{test_id}/", status_code=303)
result = job.get("result") or {}
categories: dict[str, list[dict]] = {}
for f in result.get("findings", []):
categories.setdefault(f.get("category", "other"), []).append(f)
findings = result.get("findings", [])
# Primary grouping — the 4 tabs requested by the user.
groups: dict[str, list[dict]] = {
"reliability": [],
"safety": [],
"vulnerabilities": [],
"compatibility": [],
"other": [],
}
for f in findings:
g = f.get("group") or "other"
groups.setdefault(g, []).append(f)
# Summary tab — score-impacting findings, sorted by impact desc.
impactful = sorted(
[f for f in findings if (f.get("impact") or 0) > 0],
key=lambda f: f.get("impact", 0),
reverse=True,
)
# Always include the top "good" findings too so the Summary tab is not empty
# on pristine sites. Take up to 20 impactful + 6 positives.
positives = [f for f in findings if f.get("severity") == "good"][:6]
summary = impactful[:20] + positives
# Human-friendly "finished_at" for the results header: ISO-like local string.
import datetime as _dt
finished_ts = job.get("finished_at") or job.get("started_at") or 0
finished_at_display = ""
try:
if finished_ts:
finished_at_display = _dt.datetime.fromtimestamp(int(finished_ts)).strftime("%Y-%m-%dT%H:%M:%S")
except Exception:
finished_at_display = ""
# Sharable canonical URL for the "Copy link" button. Prefer the public
# tools.* subdomain when known, otherwise the current request's origin.
share_url = str(request.url).split("#", 1)[0]
# ---- Reconstruct the "live log" that was shown on the processing page ----
# Progress entries are phase-transition messages stored in test_progress
# (ordered by seq). Findings arrive between progress rows and are grouped
# by the phase they ran under (Finding.step). We interleave the two so the
# ログ tab reads exactly like the real-time WS stream did.
progress_entries = tls_test_db.get_progress(test_id)
findings_by_step: dict[str, list[dict]] = {}
for f in findings:
step = (f.get("step") or "").strip()
findings_by_step.setdefault(step, []).append(f)
log_entries: list[dict] = []
seen_steps: set[str] = set()
for p in progress_entries:
log_entries.append({
"kind": "phase",
"phase": p.get("phase") or "",
"detail": p.get("detail") or "",
"severity": p.get("severity") or "info",
})
step = (p.get("phase") or "").strip()
if step and step not in seen_steps:
for f in findings_by_step.get(step, []):
log_entries.append({"kind": "finding", "finding": f})
seen_steps.add(step)
# Any findings whose step never showed up as a progress row (rare — engine
# errors etc.) — append them at the end so nothing is dropped.
for step, fs in findings_by_step.items():
if step not in seen_steps:
for f in fs:
log_entries.append({"kind": "finding", "finding": f})
return templates.TemplateResponse(
request=request,
name="tools/tls-test/results.html",
@@ -223,7 +307,18 @@ async def tls_test_results_page(request: Request, test_id: str) -> Response:
"test_id": test_id,
"job": job,
"result": result,
"categories": categories,
"groups": groups,
"summary": summary,
"group_labels": {
"reliability": "信頼性",
"safety": "安全性",
"vulnerabilities": "脆弱性",
"compatibility": "互換性",
"other": "その他",
},
"finished_at_display": finished_at_display,
"share_url": share_url,
"log_entries": log_entries,
},
)