Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const metadata: Metadata = {

export default function AboutPage() {
return (
<article className="prose prose-invert max-w-3xl">
<article className="max-w-3xl">
<h1 className="text-3xl font-bold tracking-tight">About</h1>
<p className="text-slate-400 mt-4 leading-relaxed">
Cyber Toolbox is a small, growing collection of single-purpose web-security utilities. The
Expand Down
6 changes: 3 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="en">
<body className="font-sans antialiased" data-nonce={nonce}>
<a href="#main" className="skip-link">Skip to main content</a>
<header className="border-b border-ink-700/60 bg-ink-950/60 backdrop-blur sticky top-0 z-10">
<header className="border-b border-ink-700/60 bg-ink-950/95 backdrop-blur sticky top-0 z-10">
<div className="mx-auto max-w-6xl px-6 py-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2 group">
<span className="inline-block h-2.5 w-2.5 rounded-full bg-accent-500 shadow-[0_0_12px_rgba(56,189,248,0.7)]" />
<span className="font-semibold tracking-tight group-hover:text-accent-400 transition">
Cyber Toolbox
</span>
</Link>
<nav className="text-sm text-slate-400 flex items-center gap-5">
<nav aria-label="Primary navigation" className="text-sm text-slate-400 flex items-center gap-5">
<Link href="/" className="hover:text-slate-200 transition">Tools</Link>
<Link href="/about" className="hover:text-slate-200 transition">About</Link>
</nav>
</div>
</header>
<main id="main" className="mx-auto max-w-6xl px-6 py-10">{children}</main>
<footer className="mx-auto max-w-6xl px-6 py-10 text-xs text-slate-500">
<footer className="mx-auto max-w-6xl px-6 py-10 text-xs text-slate-400">
<p>
For authorized testing and educational use only. Only scan systems you own or have explicit
permission to test.
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function HomePage() {
to do one thing well, explain its findings clearly, and be safe to run from a browser.
</p>
</section>
<section className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<section className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{TOOLS.map((tool) => (
<ToolCard key={tool.id} tool={tool} />
))}
Expand Down
54 changes: 40 additions & 14 deletions app/tools/cert-viewer/View.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { CertReportView } from "@/components/CertReportView";
import type { CertReport } from "@/lib/tls/types";

Expand All @@ -11,8 +11,11 @@ export default function CertViewerPage() {
const [report, setReport] = useState<CertReport | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const resultsHeadingRef = useRef<HTMLHeadingElement>(null);
const hadResult = useRef(false);

async function scan(target: string) {
if (loading) return;
setLoading(true);
setError(null);
setReport(null);
Expand All @@ -37,9 +40,16 @@ export default function CertViewerPage() {

function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (loading) return;
if (host.trim()) scan(host.trim());
}

useEffect(() => {
const hasResult = report !== null || error !== null;
if (hasResult && !hadResult.current) resultsHeadingRef.current?.focus();
hadResult.current = hasResult;
}, [report, error]);

return (
<div className="space-y-8">
<header>
Expand All @@ -58,7 +68,11 @@ export default function CertViewerPage() {
onSubmit={onSubmit}
className="flex flex-col sm:flex-row gap-2 rounded-2xl border border-ink-700 bg-ink-900/60 p-2"
>
<label htmlFor="cert-host" className="sr-only">
Hostname and port to inspect
</label>
<input
id="cert-host"
type="text"
value={host}
onChange={(e) => setHost(e.target.value)}
Expand All @@ -67,6 +81,8 @@ export default function CertViewerPage() {
autoFocus
spellCheck={false}
autoComplete="off"
aria-describedby={error ? "cert-error" : undefined}
aria-invalid={error ? true : undefined}
className="flex-1 bg-transparent px-4 py-3 font-mono text-sm text-slate-100 placeholder-slate-500 focus:outline-none"
/>
<button
Expand All @@ -79,11 +95,12 @@ export default function CertViewerPage() {
</form>

<div className="flex flex-wrap items-center gap-2 text-xs">
<span className="text-slate-500 mr-2">Try:</span>
<span className="text-slate-400 mr-2">Try:</span>
{SAMPLES.map((s) => (
<button
key={s}
type="button"
disabled={loading}
onClick={() => {
setHost(s);
scan(s);
Expand All @@ -95,20 +112,29 @@ export default function CertViewerPage() {
))}
</div>

{error && (
<div className="rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">
{error}
</div>
)}
<div role="status" aria-live="polite" aria-busy={loading}>
<h2 ref={resultsHeadingRef} tabIndex={-1} className="sr-only">
Certificate inspection results
</h2>
{error && (
<div
id="cert-error"
role="alert"
className="rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200"
>
{error}
</div>
)}

{loading && (
<div className="rounded-2xl border border-ink-700 bg-ink-900/40 p-10 text-center text-slate-400">
<div className="inline-block h-6 w-6 rounded-full border-2 border-accent-500/30 border-t-accent-500 animate-spin mb-3" />
<p className="text-sm">Performing TLS handshake…</p>
</div>
)}
{loading && (
<div className="rounded-2xl border border-ink-700 bg-ink-900/40 p-10 text-center text-slate-400">
<div className="inline-block h-6 w-6 rounded-full border-2 border-accent-500/30 border-t-accent-500 animate-spin mb-3" />
<p className="text-sm">Performing TLS handshake…</p>
</div>
)}

{report && <CertReportView report={report} />}
{report && <CertReportView report={report} />}
</div>
</div>
);
}
50 changes: 37 additions & 13 deletions app/tools/cors-tester/View.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { CorsReportView } from "@/components/CorsReportView";
import type { CorsReport } from "@/lib/cors/types";

Expand All @@ -9,9 +9,12 @@ export default function CorsTesterPage() {
const [report, setReport] = useState<CorsReport | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const resultsHeadingRef = useRef<HTMLHeadingElement>(null);
const hadResult = useRef(false);

async function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (loading) return;
setLoading(true);
setError(null);
setReport(null);
Expand All @@ -34,6 +37,12 @@ export default function CorsTesterPage() {
}
}

useEffect(() => {
const hasResult = report !== null || error !== null;
if (hasResult && !hadResult.current) resultsHeadingRef.current?.focus();
hadResult.current = hasResult;
}, [report, error]);

return (
<div className="space-y-8">
<header>
Expand All @@ -53,7 +62,11 @@ export default function CorsTesterPage() {
onSubmit={onSubmit}
className="flex flex-col sm:flex-row gap-2 rounded-2xl border border-ink-700 bg-ink-900/60 p-2"
>
<label htmlFor="cors-url" className="sr-only">
URL to test for CORS
</label>
<input
id="cors-url"
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
Expand All @@ -62,6 +75,8 @@ export default function CorsTesterPage() {
autoFocus
spellCheck={false}
autoComplete="off"
aria-describedby={error ? "cors-error" : undefined}
aria-invalid={error ? true : undefined}
className="flex-1 bg-transparent px-4 py-3 font-mono text-sm text-slate-100 placeholder-slate-500 focus:outline-none"
/>
<button
Expand All @@ -73,20 +88,29 @@ export default function CorsTesterPage() {
</button>
</form>

{error && (
<div className="rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">
{error}
</div>
)}
<div role="status" aria-live="polite" aria-busy={loading}>
<h2 ref={resultsHeadingRef} tabIndex={-1} className="sr-only">
CORS test results
</h2>
{error && (
<div
id="cors-error"
role="alert"
className="rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200"
>
{error}
</div>
)}

{loading && (
<div className="rounded-2xl border border-ink-700 bg-ink-900/40 p-10 text-center text-slate-400">
<div className="inline-block h-6 w-6 rounded-full border-2 border-accent-500/30 border-t-accent-500 animate-spin mb-3" />
<p className="text-sm">Sending Origin probes…</p>
</div>
)}
{loading && (
<div className="rounded-2xl border border-ink-700 bg-ink-900/40 p-10 text-center text-slate-400">
<div className="inline-block h-6 w-6 rounded-full border-2 border-accent-500/30 border-t-accent-500 animate-spin mb-3" />
<p className="text-sm">Sending Origin probes…</p>
</div>
)}

{report && <CorsReportView report={report} />}
{report && <CorsReportView report={report} />}
</div>
</div>
);
}
39 changes: 29 additions & 10 deletions app/tools/jwt-inspector/View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@ export default function JwtInspectorPage() {
}

async function onVerify() {
if (!parsed.ok) return;
if (!parsed.ok || verifyState.kind === "checking") return;
setVerifyState({ kind: "checking" });
const r = await verifyWithSecret(parsed.jwt, secret);
setVerifyState({ kind: "result", ok: r.verified, reason: r.reason });
}

async function onCrack() {
if (!parsed.ok) return;
if (!parsed.ok || crackState.kind === "running") return;
setCrackState({ kind: "running", tried: 0, total: 0 });
try {
const list = await loadWordlist();
setCrackState({ kind: "running", tried: 0, total: list.length });
Expand Down Expand Up @@ -126,7 +127,7 @@ export default function JwtInspectorPage() {
</header>

<div className="flex flex-wrap items-center gap-2 text-xs">
<span className="text-slate-500 mr-2">Try a sample:</span>
<span className="text-slate-400 mr-2">Try a sample:</span>
{SAMPLES.map((s) => (
<button
key={s.label}
Expand All @@ -149,18 +150,28 @@ export default function JwtInspectorPage() {
)}
</div>

<label htmlFor="jwt-token" className="sr-only">
JSON Web Token
</label>
<textarea
id="jwt-token"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Paste a JWT (eyJ…)"
spellCheck={false}
autoComplete="off"
aria-describedby={input && !parsed.ok ? "jwt-error" : undefined}
aria-invalid={input && !parsed.ok ? true : undefined}
rows={4}
className="w-full rounded-2xl border border-ink-700 bg-ink-900/60 px-4 py-3 font-mono text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-accent-500/60 resize-y"
/>

{input && !parsed.ok && (
<div className="rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200">
<div
id="jwt-error"
role="alert"
className="rounded-xl border border-rose-500/40 bg-rose-500/10 px-4 py-3 text-sm text-rose-200"
>
{parsed.reason}
</div>
)}
Expand Down Expand Up @@ -220,17 +231,22 @@ export default function JwtInspectorPage() {
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-300">
Verify with secret
</h2>
<p className="text-xs text-slate-500 mt-1">
<p className="text-xs text-slate-400 mt-1">
HMAC verification uses the browser&apos;s Web Crypto API.
</p>
<div className="mt-3 flex flex-col sm:flex-row gap-2">
<label htmlFor="jwt-secret" className="sr-only">
HMAC secret
</label>
<input
id="jwt-secret"
type="text"
value={secret}
onChange={(e) => setSecret(e.target.value)}
placeholder="HS256 secret"
spellCheck={false}
autoComplete="off"
aria-describedby={verifyState.kind === "result" ? "jwt-verify-result" : undefined}
className="flex-1 rounded-xl border border-ink-700 bg-ink-950/60 px-3 py-2 font-mono text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-accent-500/60"
/>
<button
Expand All @@ -244,6 +260,9 @@ export default function JwtInspectorPage() {
</div>
{verifyState.kind === "result" && (
<p
id="jwt-verify-result"
role="status"
aria-live="polite"
className={`mt-3 text-sm ${
verifyState.ok ? "text-emerald-300" : "text-rose-300"
}`}
Expand All @@ -261,7 +280,7 @@ export default function JwtInspectorPage() {
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-300">
Try common secrets
</h2>
<p className="text-xs text-slate-500 mt-1">
<p className="text-xs text-slate-400 mt-1">
Loads a small wordlist (~100 entries) and HMAC-verifies the token against each
one in the browser. Useful for catching dev/test secrets that shipped to prod.
</p>
Expand All @@ -275,12 +294,12 @@ export default function JwtInspectorPage() {
</button>

{crackState.kind === "running" && (
<p className="mt-3 text-xs text-slate-400 font-mono">
<p role="status" aria-live="polite" className="mt-3 text-xs text-slate-400 font-mono">
{crackState.tried} / {crackState.total} candidates…
</p>
)}
{crackState.kind === "done" && crackState.secret !== null && (
<div className="mt-3 rounded-xl border border-rose-500/40 bg-rose-500/10 px-3 py-2">
<div role="status" aria-live="polite" className="mt-3 rounded-xl border border-rose-500/40 bg-rose-500/10 px-3 py-2">
<p className="text-rose-200 text-sm font-semibold">Secret found!</p>
<p className="text-xs text-rose-200/80 mt-1">
The token was signed with{" "}
Expand All @@ -292,13 +311,13 @@ export default function JwtInspectorPage() {
</div>
)}
{crackState.kind === "done" && crackState.secret === null && (
<p className="mt-3 text-sm text-slate-400">
<p role="status" aria-live="polite" className="mt-3 text-sm text-slate-400">
No match in the small built-in wordlist. (Tried {crackState.tried} in{" "}
{crackState.durationMs} ms — this doesn&apos;t mean the secret is strong.)
</p>
)}
{crackState.kind === "error" && (
<p className="mt-3 text-sm text-rose-300">{crackState.message}</p>
<p role="alert" className="mt-3 text-sm text-rose-300">{crackState.message}</p>
)}
</div>
</section>
Expand Down
Loading
Loading