Skip to content

Commit

Permalink
Add subdomains finder 🕵️‍♀️
Browse files Browse the repository at this point in the history
  • Loading branch information
wotschofsky committed Jan 13, 2024
1 parent 957e02f commit 80b1519
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 0 deletions.
25 changes: 25 additions & 0 deletions app/lookup/[domain]/subdomains/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';

import { type FC, useEffect } from 'react';

type SubdomainsErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};

const SubdomainsError: FC<SubdomainsErrorProps> = ({ error }) => {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="mt-12 flex flex-col items-center gap-2">
<h2>Something went wrong!</h2>
<p className="mt-2 text-center text-sm text-muted-foreground">
Digest: {error.digest}
</p>
</div>
);
};

export default SubdomainsError;
9 changes: 9 additions & 0 deletions app/lookup/[domain]/subdomains/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Spinner } from '@/components/ui/spinner';

const SubdomainsLoading = () => (
<div className="flex items-center justify-center">
<Spinner className="my-8" />
</div>
);

export default SubdomainsLoading;
126 changes: 126 additions & 0 deletions app/lookup/[domain]/subdomains/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { CheckIcon, XIcon } from 'lucide-react';
import type { FC } from 'react';

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';

import DomainLink from '@/components/DomainLink';
import CloudflareDoHResolver from '@/lib/resolvers/CloudflareDoHResolver';

type CertsData = {
issuer_ca_id: number;
issuer_name: string;
common_name: string;
name_value: string;
id: number;
entry_timestamp: string;
not_before: string;
not_after: string;
serial_number: string;
}[];

export const runtime = 'edge';
// crt.sh located in GB, always use LHR1 for lowest latency
export const preferredRegion = 'lhr1';

const lookupCerts = async (domain: string): Promise<CertsData> => {
const response = await fetch(
'https://crt.sh?' +
new URLSearchParams({
Identity: domain,
output: 'json',
})
);

if (!response.ok) {
throw new Error('Failed to fetch certs');
}

return await response.json();
};

type SubdomainsResultsPageProps = {
params: {
domain: string;
};
};

const SubdomainsResultsPage: FC<SubdomainsResultsPageProps> = async ({
params: { domain },
}) => {
const resolver = new CloudflareDoHResolver();

const certs = await lookupCerts(domain);

const issuedCerts = certs.map((cert) => ({
date: new Date(cert.entry_timestamp),
domains: [cert.common_name, ...cert.name_value.split(/\n/g)],
}));

const uniqueDomains = Array.from(
new Set<string>(issuedCerts.flatMap((r) => r.domains))
).filter((d) => d.endsWith(`.${domain}`));

const results = await Promise.all(
uniqueDomains.map(async (domain, i) => {
const records = await resolver.resolveAllRecords(domain);
const hasRecords = Object.values(records).some((r) => r.length > 0);

return {
domain,
firstSeen: issuedCerts
.filter((c) => c.domains.includes(domain))
.sort((a, b) => a.date.getTime() - b.date.getTime())[0].date,
stillExists: hasRecords,
};
})
);
const sortedResults = results.sort(
(a, b) => b.firstSeen.getTime() - a.firstSeen.getTime()
);

if (!sortedResults.length) {
return (
<p className="mt-8 text-center text-muted-foreground">
Could not find any subdomains for this domain.
</p>
);
}

return (
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="pl-0">Domain Name</TableHead>
<TableHead>First seen</TableHead>
<TableHead className="pr-0">Still exists</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedResults.map((result) => (
<TableRow key={result.domain} className="hover:bg-transparent">
<TableCell className="pl-0">
<DomainLink domain={result.domain} />
</TableCell>
<TableCell>{result.firstSeen.toISOString()}</TableCell>
<TableCell className="pr-0">
{result.stillExists ? (
<CheckIcon size="1.25rem" />
) : (
<XIcon size="1.25rem" />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};

export default SubdomainsResultsPage;
9 changes: 9 additions & 0 deletions components/ResultsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ const ResultsTabs: FC<ResultsTabsProps> = ({ domain }) => {
useHotkeys('alt+2', () => router.push(`/lookup/${domain}/map`), [router]);
useHotkeys('alt+3', () => router.push(`/lookup/${domain}/whois`), [router]);
useHotkeys('alt+4', () => router.push(`/lookup/${domain}/certs`), [router]);
useHotkeys('alt+5', () => router.push(`/lookup/${domain}/subdomains`), [
router,
]);

return (
<div className="group mb-6 mt-6 border-b border-gray-200 text-center text-sm font-medium text-gray-500 dark:border-gray-700 dark:text-gray-400">
Expand Down Expand Up @@ -82,6 +85,12 @@ const ResultsTabs: FC<ResultsTabsProps> = ({ domain }) => {
selected={selectedSegment === 'certs'}
shortcutNumber={4}
/>
<SingleTab
label="Subdomains"
href={`/lookup/${domain}/subdomains`}
selected={selectedSegment === 'subdomains'}
shortcutNumber={5}
/>
</ul>
</div>
);
Expand Down

1 comment on commit 80b1519

@vercel
Copy link

@vercel vercel bot commented on 80b1519 Jan 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.